diff --git a/WheelWizard.Test/Views/ModPreviewRetryTests.cs b/WheelWizard.Test/Features/Mods/ViewModels/ModPreviewRetryTests.cs similarity index 98% rename from WheelWizard.Test/Views/ModPreviewRetryTests.cs rename to WheelWizard.Test/Features/Mods/ViewModels/ModPreviewRetryTests.cs index 07f622add..c007c6fea 100644 --- a/WheelWizard.Test/Views/ModPreviewRetryTests.cs +++ b/WheelWizard.Test/Features/Mods/ViewModels/ModPreviewRetryTests.cs @@ -3,10 +3,10 @@ using SixLabors.ImageSharp.PixelFormats; using WheelWizard.GameBanana; using WheelWizard.GameBanana.Domain; +using WheelWizard.Mods.ViewModels; using WheelWizard.Shared; -using WheelWizard.Views.ModManagement; -namespace WheelWizard.Test.Views; +namespace WheelWizard.Test.Features.Mods.ViewModels; public class ModPreviewRetryTests { diff --git a/WheelWizard.Test/Views/ModPreviewViewModelTests.cs b/WheelWizard.Test/Features/Mods/ViewModels/ModPreviewViewModelTests.cs similarity index 97% rename from WheelWizard.Test/Views/ModPreviewViewModelTests.cs rename to WheelWizard.Test/Features/Mods/ViewModels/ModPreviewViewModelTests.cs index 63d4ae66c..581fb0000 100644 --- a/WheelWizard.Test/Views/ModPreviewViewModelTests.cs +++ b/WheelWizard.Test/Features/Mods/ViewModels/ModPreviewViewModelTests.cs @@ -1,9 +1,9 @@ using WheelWizard.GameBanana; using WheelWizard.GameBanana.Domain; +using WheelWizard.Mods.ViewModels; using WheelWizard.Shared; -using WheelWizard.Views.ModManagement; -namespace WheelWizard.Test.Views; +namespace WheelWizard.Test.Features.Mods.ViewModels; public class ModPreviewViewModelTests { diff --git a/WheelWizard.Test/Views/VrHistoryViewModelTests.cs b/WheelWizard.Test/Features/WheelWizardData/ViewModels/VrHistoryViewModelTests.cs similarity index 97% rename from WheelWizard.Test/Views/VrHistoryViewModelTests.cs rename to WheelWizard.Test/Features/WheelWizardData/ViewModels/VrHistoryViewModelTests.cs index 29d2fc9a3..a29b51acb 100644 --- a/WheelWizard.Test/Views/VrHistoryViewModelTests.cs +++ b/WheelWizard.Test/Features/WheelWizardData/ViewModels/VrHistoryViewModelTests.cs @@ -2,9 +2,9 @@ using WheelWizard.RrRooms; using WheelWizard.Shared; using WheelWizard.Shared.Services; -using WheelWizard.Views.Patterns; +using WheelWizard.WheelWizardData.ViewModels; -namespace WheelWizard.Test.Views; +namespace WheelWizard.Test.Features.WheelWizardData.ViewModels; public class VrHistoryViewModelTests { diff --git a/WheelWizard.Test/Views/Storage/FilePickerServiceTests.cs b/WheelWizard.Test/Shared/Desktop/Storage/FilePickerServiceTests.cs similarity index 97% rename from WheelWizard.Test/Views/Storage/FilePickerServiceTests.cs rename to WheelWizard.Test/Shared/Desktop/Storage/FilePickerServiceTests.cs index a963004f0..e6a0d0dbe 100644 --- a/WheelWizard.Test/Views/Storage/FilePickerServiceTests.cs +++ b/WheelWizard.Test/Shared/Desktop/Storage/FilePickerServiceTests.cs @@ -1,10 +1,10 @@ using System.Diagnostics; using Avalonia.Platform.Storage; +using WheelWizard.Shared.Desktop.Storage; using WheelWizard.Shared.Platform; using WheelWizard.Shared.Processes; -using WheelWizard.Views.Storage; -namespace WheelWizard.Test.Views.Storage; +namespace WheelWizard.Test.Shared.Desktop.Storage; public class FilePickerServiceTests { diff --git a/WheelWizard.Test/Shared/Polling/ObservablePollingServiceTests.cs b/WheelWizard.Test/Shared/Polling/ObservablePollingServiceTests.cs index 855b347f8..49c286a8d 100644 --- a/WheelWizard.Test/Shared/Polling/ObservablePollingServiceTests.cs +++ b/WheelWizard.Test/Shared/Polling/ObservablePollingServiceTests.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging.Abstractions; using WheelWizard.Shared.Polling; -using WheelWizard.Views.Diagnostics; +using WheelWizard.Views.DesignTime.Diagnostics; namespace WheelWizard.Test.Shared.Polling; diff --git a/WheelWizard.Test/Views/NavigationTests.cs b/WheelWizard.Test/Views/Shell/Navigation/NavigationTests.cs similarity index 96% rename from WheelWizard.Test/Views/NavigationTests.cs rename to WheelWizard.Test/Views/Shell/Navigation/NavigationTests.cs index 52123cf4a..f9eb2114b 100644 --- a/WheelWizard.Test/Views/NavigationTests.cs +++ b/WheelWizard.Test/Views/Shell/Navigation/NavigationTests.cs @@ -1,8 +1,8 @@ using Avalonia.Controls; using Microsoft.Extensions.DependencyInjection; -using WheelWizard.Views.Navigation; +using WheelWizard.Views.Shell.Navigation; -namespace WheelWizard.Test.Views; +namespace WheelWizard.Test.Views.Shell.Navigation; public class NavigationTests { diff --git a/WheelWizard.Test/Views/DesktopStartupTests.cs b/WheelWizard.Test/Views/Shell/Startup/DesktopStartupTests.cs similarity index 96% rename from WheelWizard.Test/Views/DesktopStartupTests.cs rename to WheelWizard.Test/Views/Shell/Startup/DesktopStartupTests.cs index af04fea41..b046a6918 100644 --- a/WheelWizard.Test/Views/DesktopStartupTests.cs +++ b/WheelWizard.Test/Views/Shell/Startup/DesktopStartupTests.cs @@ -4,11 +4,11 @@ using WheelWizard.AutoUpdating; using WheelWizard.MiiRendering.Services; using WheelWizard.Shared; -using WheelWizard.Views; -using WheelWizard.Views.Startup; +using WheelWizard.Views.Shell; +using WheelWizard.Views.Shell.Startup; using WheelWizard.WiiManagement.GameLicense; -namespace WheelWizard.Test.Views; +namespace WheelWizard.Test.Views.Shell.Startup; public class DesktopStartupTests { diff --git a/WheelWizard.Test/Views/HomeDolphinLaunchTests.cs b/WheelWizard.Test/Views/Shell/ViewModels/HomeDolphinLaunchTests.cs similarity index 98% rename from WheelWizard.Test/Views/HomeDolphinLaunchTests.cs rename to WheelWizard.Test/Views/Shell/ViewModels/HomeDolphinLaunchTests.cs index 1709e38b5..d35b615b4 100644 --- a/WheelWizard.Test/Views/HomeDolphinLaunchTests.cs +++ b/WheelWizard.Test/Views/Shell/ViewModels/HomeDolphinLaunchTests.cs @@ -12,10 +12,11 @@ using WheelWizard.Shared.Calendar; using WheelWizard.Shared.Platform; using WheelWizard.Shared.Processes; -using WheelWizard.Views.Pages; +using WheelWizard.Views.Shell; +using WheelWizard.Views.Shell.ViewModels; using TimeProvider = System.TimeProvider; -namespace WheelWizard.Test.Views; +namespace WheelWizard.Test.Views.Shell.ViewModels; public class HomeDolphinLaunchTests { diff --git a/WheelWizard.Test/Views/HomeViewModelTests.cs b/WheelWizard.Test/Views/Shell/ViewModels/HomeViewModelTests.cs similarity index 98% rename from WheelWizard.Test/Views/HomeViewModelTests.cs rename to WheelWizard.Test/Views/Shell/ViewModels/HomeViewModelTests.cs index c4f9cace1..17627a391 100644 --- a/WheelWizard.Test/Views/HomeViewModelTests.cs +++ b/WheelWizard.Test/Views/Shell/ViewModels/HomeViewModelTests.cs @@ -4,9 +4,9 @@ using WheelWizard.Settings; using WheelWizard.Shared; using WheelWizard.Shared.Calendar; -using WheelWizard.Views.Pages; +using WheelWizard.Views.Shell.ViewModels; -namespace WheelWizard.Test.Views; +namespace WheelWizard.Test.Views.Shell.ViewModels; public class HomeViewModelTests { diff --git a/WheelWizard.UI.Test/ApplicationCompositionTests.cs b/WheelWizard.UI.Test/ApplicationCompositionTests.cs index f6f12d54c..23e56aa22 100644 --- a/WheelWizard.UI.Test/ApplicationCompositionTests.cs +++ b/WheelWizard.UI.Test/ApplicationCompositionTests.cs @@ -12,17 +12,23 @@ using NSubstitute; using Testably.Abstractions.Testing; using WheelWizard.ApplicationData; +using WheelWizard.MiiImages.Views; using WheelWizard.Mods; +using WheelWizard.Mods.Views; +using WheelWizard.RrRooms.Views; using WheelWizard.Settings; +using WheelWizard.Settings.Views; using WheelWizard.Shared; using WheelWizard.Shared.Services; -using WheelWizard.Views; using WheelWizard.Views.Components; -using WheelWizard.Views.Navigation; -using WheelWizard.Views.Pages; -using WheelWizard.Views.Pages.Settings; -using WheelWizard.Views.Patterns; -using WheelWizard.Views.Startup; +using WheelWizard.Views.DesignTime; +using WheelWizard.Views.Shell; +using WheelWizard.Views.Shell.Controls; +using WheelWizard.Views.Shell.Navigation; +using WheelWizard.Views.Shell.Startup; +using WheelWizard.Views.Shell.Views; +using WheelWizard.WheelWizardData.Views; +using WheelWizard.WiiManagement.MiiManagement.Views; using Button = Avalonia.Controls.Button; namespace WheelWizard.UI.Test; @@ -235,7 +241,7 @@ public async Task MainWindowAndPages_ConstructWithoutStaticServiceInitialization Assert.Equal(string.Empty, roomsButton.GetVisualDescendants().OfType().Single().Text); Assert.Equal(19.5, roomsButton.GetVisualDescendants().OfType().Single().Margin.Left); Assert.Equal(0, original.FindControl("OtherSectionText")!.Opacity); - Assert.False(roomsButton.GetVisualDescendants().OfType().Single().IsVisible); + Assert.False(roomsButton.GetVisualDescendants().OfType().Single().IsVisible); toggle.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); original.UpdateLayout(); Assert.Equal(221, original.SidebarWidth); @@ -273,6 +279,35 @@ public async Task MainWindowAndPages_ConstructWithoutStaticServiceInitialization navigation.NavigateTo(page); original.UpdateLayout(); Assert.IsType(page, navigation.CurrentPage); + if (navigation.CurrentPage is ModsPage modsPage) + { + modsPage.HasMods = true; + original.UpdateLayout(); + var gridView = modsPage.FindControl("GridViewButton")!; + var listView = modsPage.FindControl("ListViewButton")!; + var list = modsPage.FindControl("ModsListBox")!; + var grid = modsPage.FindControl("ModsGridView")!; + gridView.IsChecked = true; + Assert.True(grid.IsVisible); + Assert.False(list.IsVisible); + listView.IsChecked = true; + Assert.True(list.IsVisible); + Assert.False(grid.IsVisible); + original.UpdateLayout(); + var viewToggle = modsPage.FindControl("ViewModeToggle")!; + Assert.True( + viewToggle.TranslatePoint(new Point(), modsPage)!.Value.Y > list.TranslatePoint(new Point(), modsPage)!.Value.Y + ); + var enableAll = modsPage.FindControl("EnableAllCheckbox")!; + Assert.True( + enableAll.TranslatePoint(new Point(), modsPage)!.Value.Y < list.TranslatePoint(new Point(), modsPage)!.Value.Y + ); + var presenter = list.GetVisualDescendants().OfType().Single(); + Assert.True(presenter.Margin.Bottom >= viewToggle.Bounds.Height + 12); + var icon = listView.GetVisualDescendants().OfType().Single(); + Assert.Equal(Avalonia.Media.Brushes.Transparent, icon.Fill); + Assert.NotNull(icon.Stroke); + } } var room = new WheelWizard.Models.RRInfo.RrRoom { diff --git a/WheelWizard.UI.Test/BadgeBindingTests.cs b/WheelWizard.UI.Test/BadgeBindingTests.cs index 025cc49ac..056c62fb2 100644 --- a/WheelWizard.UI.Test/BadgeBindingTests.cs +++ b/WheelWizard.UI.Test/BadgeBindingTests.cs @@ -2,9 +2,10 @@ using Avalonia.Controls.Primitives; using Avalonia.Headless.XUnit; using Avalonia.VisualTree; +using WheelWizard.RrRooms.Views; using WheelWizard.Views.Components; -using WheelWizard.Views.Patterns; using WheelWizard.WheelWizardData.Domain; +using WheelWizard.WheelWizardData.Views; namespace WheelWizard.UI.Test; @@ -29,7 +30,7 @@ public void Cards_DisplayAndRefreshSuppliedBadgesWithoutServiceLookup(bool isFri { window.Show(); window.UpdateLayout(); - Assert.Equal(initial, card.GetVisualDescendants().OfType().Select(badge => badge.Variant)); + Assert.Equal(initial, card.GetVisualDescendants().OfType().Select(badge => badge.Variant)); BadgeVariant[] replacement = [BadgeVariant.RrDev]; if (card is FriendsListItem friend) @@ -37,7 +38,7 @@ public void Cards_DisplayAndRefreshSuppliedBadgesWithoutServiceLookup(bool isFri else ((PlayerListItem)card).BadgeVariants = replacement; window.UpdateLayout(); - Assert.Equal(replacement, card.GetVisualDescendants().OfType().Select(badge => badge.Variant)); + Assert.Equal(replacement, card.GetVisualDescendants().OfType().Select(badge => badge.Variant)); } finally { diff --git a/WheelWizard.UI.Test/MiiControlTests.cs b/WheelWizard.UI.Test/MiiControlTests.cs index 0f36747a0..36902978d 100644 --- a/WheelWizard.UI.Test/MiiControlTests.cs +++ b/WheelWizard.UI.Test/MiiControlTests.cs @@ -7,10 +7,11 @@ using NSubstitute; using WheelWizard.MiiImages; using WheelWizard.MiiImages.Domain; +using WheelWizard.MiiImages.Views; using WheelWizard.MiiRendering.Services; using WheelWizard.Shared; using WheelWizard.Shared.Calendar; -using WheelWizard.Views.Patterns; +using WheelWizard.WheelWizardData.Views; using WheelWizard.WiiManagement.MiiManagement; using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; diff --git a/WheelWizard.UI.Test/ModBrowserPreviewTests.cs b/WheelWizard.UI.Test/ModBrowserPreviewTests.cs index 55ef948b2..03dd994ae 100644 --- a/WheelWizard.UI.Test/ModBrowserPreviewTests.cs +++ b/WheelWizard.UI.Test/ModBrowserPreviewTests.cs @@ -1,23 +1,83 @@ +using Avalonia; using Avalonia.Controls; +using Avalonia.Headless; using Avalonia.Headless.XUnit; using Avalonia.Threading; using Avalonia.VisualTree; using NSubstitute; using WheelWizard.GameBanana; using WheelWizard.GameBanana.Domain; +using WheelWizard.Models.Mods; using WheelWizard.Mods; +using WheelWizard.Mods.ViewModels; +using WheelWizard.Mods.Views; +using WheelWizard.Mods.Views.Dialogs; using WheelWizard.Shared; using WheelWizard.Shared.Downloads; using WheelWizard.Shared.Services; -using WheelWizard.Views.ModManagement; -using WheelWizard.Views.Navigation; -using WheelWizard.Views.Patterns; -using WheelWizard.Views.Popups.ModManagement; +using WheelWizard.Views.Components; +using WheelWizard.Views.Shell.Navigation; namespace WheelWizard.UI.Test; public class ModBrowserPreviewTests { + [AvaloniaFact] + public void DisabledGridCard_RestoresColorOnWholeCardHover_AndTracksEnabledState() + { + using var preview = new ModPreviewViewModel( + 0, + Substitute.For(), + Substitute.For() + ); + var mod = new Mod + { + Title = "Test mod", + IsEnabled = false, + HasIncompatibleFiles = true, + }; + var card = new GridModPanel { DataContext = new ModListItem(mod, false, false, preview) }; + var window = new Window + { + Content = card, + Width = 300, + Height = 220, + }; + try + { + window.Show(); + window.UpdateLayout(); + var grayImage = card.FindControl("DisabledThumbnailImage")!; + var originalSize = card.FindControl("RootCardBorder")!.Bounds.Size; + Assert.True(grayImage.IsVisible); + Assert.True(card.FindControl("CompatibilityWarning")!.IsVisible); + window.MouseMove(new Point(25, 130)); + Assert.False(grayImage.IsVisible); + window.MouseMove(card.FindControl("ThumbnailHeader")!.TranslatePoint(new Point(20, 40), window)!.Value); + Assert.False(grayImage.IsVisible); + window.MouseMove(new Point(25, 130)); + Assert.False(grayImage.IsVisible); + window.MouseDown(new Point(25, 130), Avalonia.Input.MouseButton.Left); + window.MouseUp(new Point(25, 130), Avalonia.Input.MouseButton.Left); + Assert.True(mod.IsEnabled); + window.UpdateLayout(); + Assert.Equal(originalSize, card.FindControl("RootCardBorder")!.Bounds.Size); + window.MouseDown(new Point(25, 130), Avalonia.Input.MouseButton.Left); + window.MouseUp(new Point(25, 130), Avalonia.Input.MouseButton.Left); + Assert.False(mod.IsEnabled); + window.MouseMove(new Point(1, 1)); + Assert.True(grayImage.IsVisible); + mod.IsEnabled = true; + Assert.False(grayImage.IsVisible); + mod.IsEnabled = false; + Assert.True(grayImage.IsVisible); + } + finally + { + window.Close(); + } + } + [AvaloniaFact] public async Task ClosedBrowser_DiscardsPendingSearchResults() { @@ -219,7 +279,10 @@ public async Task RecycledCard_LoadsAndBindsItsCurrentPreview() { var mods = Substitute.For(); var media = Substitute.For(); - var png = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jvX0AAAAASUVORK5CYII="); + using var pixels = new SkiaSharp.SKBitmap(2, 2); + pixels.Erase(SkiaSharp.SKColors.Red); + using var encoded = pixels.Encode(SkiaSharp.SKEncodedImageFormat.Png, 100); + var png = encoded.ToArray(); media.GetImageAsync(Arg.Any(), Arg.Any()).Returns((OperationResult)png); using var first = new ModPreviewViewModel(1, mods, media, "https://images.test/first.png"); using var second = new ModPreviewViewModel(2, mods, media, "https://images.test/second.png"); @@ -238,6 +301,9 @@ public async Task RecycledCard_LoadsAndBindsItsCurrentPreview() await first.LoadAsync(); var image = Assert.Single(card.GetVisualDescendants().OfType()); Assert.NotNull(first.Image); + Assert.NotNull(first.GrayscaleImage); + Assert.NotSame(first.Image, first.GrayscaleImage); + Assert.Same(first.GrayscaleImage, first.GrayscaleImage); Assert.Same(first.Image, image.Source); card.Preview = second; diff --git a/WheelWizard.UI.Test/SharedControlTests.cs b/WheelWizard.UI.Test/SharedControlTests.cs new file mode 100644 index 000000000..612187f9f --- /dev/null +++ b/WheelWizard.UI.Test/SharedControlTests.cs @@ -0,0 +1,240 @@ +using System.ComponentModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Headless; +using Avalonia.Headless.XUnit; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Media; +using Avalonia.Threading; +using Avalonia.VisualTree; +using WheelWizard.Shared; +using WheelWizard.Views.Components; +using WheelWizard.Views.Dialogs; +using WheelWizard.Views.Shell.Controls; +using Button = WheelWizard.Views.Components.Button; + +namespace WheelWizard.UI.Test; + +public class SharedControlTests +{ + [AvaloniaFact] + public void Button_RendersNativeContentAndOpensItsFlyoutFromKeyboard() + { + var content = new TextBlock { Text = "Custom content" }; + var menu = new MenuFlyout(); + menu.Items.Add(new MenuItem { Header = "Import" }); + var button = new Button + { + Content = content, + Flyout = menu, + Variant = ButtonVariant.Primary, + }; + button.Classes.Add("caller-class"); + var window = new Window { Content = button }; + try + { + window.Show(); + window.UpdateLayout(); + Assert.Contains(content, button.GetVisualDescendants()); + var initialBackground = button.Background; + button.Variant = ButtonVariant.Danger; + Assert.NotEqual(initialBackground, button.Background); + Assert.Equal(new[] { "caller-class" }, button.Classes); + button.Focus(); + window.KeyPress(Key.Space, RawInputModifiers.None, PhysicalKey.Space, " "); + window.KeyRelease(Key.Space, RawInputModifiers.None, PhysicalKey.Space, " "); + Assert.True(menu.IsOpen); + menu.Hide(); + Assert.False(menu.IsOpen); + } + finally + { + window.Close(); + } + } + + [AvaloniaFact] + public void SidebarButton_NavigatesFromKeyboardActivation() + { + var button = new SidebarRadioButton { PageType = typeof(TextInputWindow) }; + Type? requested = null; + button.NavigationRequested += (_, page) => requested = page; + var window = new Window { Content = button }; + try + { + window.Show(); + window.UpdateLayout(); + button.Focus(); + window.KeyPress(Key.Space, RawInputModifiers.None, PhysicalKey.Space, " "); + Assert.Null(requested); + window.KeyRelease(Key.Space, RawInputModifiers.None, PhysicalKey.Space, " "); + Assert.Equal(typeof(TextInputWindow), requested); + Assert.True(button.IsChecked); + } + finally + { + window.Close(); + } + } + + [AvaloniaFact] + public void TextField_PreservesCallerBindingAndDerivesErrorsFromText() + { + var model = new InputModel(); + var field = new TextField { DataContext = model, ErrorText = "Invalid value" }; + field.Bind(TextField.TextProperty, new Binding(nameof(InputModel.Value))); + var window = new Window + { + Content = field, + Width = 320, + Height = 160, + }; + try + { + window.Show(); + window.UpdateLayout(); + var input = Assert.Single(field.GetVisualDescendants().OfType()); + Assert.Same(model, field.DataContext); + Assert.Equal("Initial", input.Text); + Assert.True(field.HasError); + Assert.Contains("error", input.Classes); + input.SetCurrentValue(TextBox.TextProperty, "Edited"); + Dispatcher.UIThread.RunJobs(); + Assert.Equal("Edited", model.Value); + model.Value = "Updated"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal("Updated", input.Text); + foreach (var cleared in new string?[] { null, "", " " }) + { + field.ErrorText = cleared; + Assert.False(field.HasError); + Assert.DoesNotContain("error", input.Classes); + } + field.ErrorText = "Duplicate friend code"; + Assert.True(field.HasError); + Assert.Contains("error", input.Classes); + } + finally + { + window.Close(); + } + } + + [AvaloniaFact] + public void TextInput_ValidationImmediatelyDisablesInvalidInitialValues() + { + var dialog = new TextInputWindow() + .SetInitialText("duplicate") + .SetValidation((_, text) => text == "duplicate" ? OperationError.Fail("Already in your list") : OperationResult.Ok()); + var field = dialog.FindControl("InputField")!; + var submit = dialog.FindControl