using Avalonia.Controls; using AvParser.Core.Settings; using AvParser.UI.Navigation; using AvParser.UI.Responsive; using AvParser.UI.Tests.Fakes; using AvParser.UI.ViewModels; using ReactiveUI.Primitives; using ReactiveUI.Primitives.Concurrency; namespace AvParser.UI.Tests; public class ShellViewModelTests { private static (ShellViewModel Shell, NavigationService Navigation, FakeThemeService Theme) Build( AppTheme theme = AppTheme.System ) { var navigation = new NavigationService([new FakePage("First"), new FakePage("Second"), new OtherFakePage()]); var themeService = new FakeThemeService(theme); // ImmediateSequencer makes every derived property settle before the next line runs, // which is what lets these read as plain synchronous assertions. return (new ShellViewModel(navigation, themeService, ImmediateSequencer.Instance), navigation, themeService); } [Theory] [InlineData(Breakpoint.Expanded, SplitViewDisplayMode.Inline)] [InlineData(Breakpoint.Medium, SplitViewDisplayMode.CompactInline)] [InlineData(Breakpoint.Compact, SplitViewDisplayMode.Overlay)] public void Breakpoint_selects_the_pane_display_mode(Breakpoint breakpoint, SplitViewDisplayMode expected) { var (shell, _, _) = Build(); shell.Breakpoint = breakpoint; shell.PaneDisplayMode.ShouldBe(expected); } [Theory] [InlineData(Breakpoint.Expanded, true)] [InlineData(Breakpoint.Medium, false)] [InlineData(Breakpoint.Compact, false)] public void Crossing_a_breakpoint_resets_the_pane(Breakpoint breakpoint, bool expectedOpen) { var (shell, _, _) = Build(); shell.Breakpoint = breakpoint; shell.IsPaneOpen.ShouldBe(expectedOpen); } [Fact] public void A_manual_toggle_survives_until_the_next_breakpoint_change() { var (shell, _, _) = Build(); shell.Breakpoint = Breakpoint.Compact; shell.TogglePaneCommand.Execute().Subscribe(_ => { }); shell.IsPaneOpen.ShouldBeTrue(); shell.Breakpoint = Breakpoint.Expanded; shell.IsPaneOpen.ShouldBeTrue(); shell.Breakpoint = Breakpoint.Compact; shell.IsPaneOpen.ShouldBeFalse(); } [Fact] public void Choosing_a_destination_dismisses_the_compact_drawer() { var (shell, _, _) = Build(); shell.Breakpoint = Breakpoint.Compact; shell.TogglePaneCommand.Execute().Subscribe(_ => { }); shell.IsPaneOpen.ShouldBeTrue(); shell.SelectedPage = shell.Pages[1]; shell.IsPaneOpen.ShouldBeFalse(); } [Fact] public void Choosing_a_destination_leaves_the_expanded_sidebar_open() { var (shell, _, _) = Build(); shell.Breakpoint = Breakpoint.Expanded; shell.SelectedPage = shell.Pages[1]; shell.IsPaneOpen.ShouldBeTrue(); } [Fact] public void Selecting_a_page_navigates_to_it() { var (shell, navigation, _) = Build(); shell.SelectedPage = shell.Pages[2]; navigation.Current.ShouldBeSameAs(shell.Pages[2]); shell.CurrentPage.ShouldBeSameAs(shell.Pages[2]); shell.Title.ShouldBe("Other"); } [Fact] public void Navigating_from_elsewhere_updates_the_rail_selection() { var (shell, navigation, _) = Build(); navigation.NavigateTo(); shell.SelectedPage.ShouldBeSameAs(navigation.Current); } [Fact] public void Back_is_disabled_until_something_is_on_the_stack() { var (shell, _, _) = Build(); shell.CanGoBack.ShouldBeFalse(); shell.SelectedPage = shell.Pages[1]; shell.CanGoBack.ShouldBeTrue(); } [Fact] public void Back_returns_to_the_previous_page() { var (shell, _, _) = Build(); var first = shell.Pages[0]; shell.SelectedPage = shell.Pages[1]; shell.GoBackCommand.Execute().Subscribe(_ => { }); shell.CurrentPage.ShouldBeSameAs(first); shell.SelectedPage.ShouldBeSameAs(first); } [Theory] [InlineData(AppTheme.Dark, AppTheme.Light)] [InlineData(AppTheme.Light, AppTheme.Dark)] [InlineData(AppTheme.System, AppTheme.Dark)] public void Toggling_the_theme_flips_between_light_and_dark(AppTheme start, AppTheme expected) { var (shell, _, theme) = Build(start); shell.ToggleThemeCommand.Execute().Subscribe(_ => { }); theme.Current.ShouldBe(expected); } [Fact] public void The_theme_button_shows_the_theme_it_would_switch_to() { var (shell, _, theme) = Build(AppTheme.Light); shell.ThemeIconKey.ShouldBe("IconMoon"); theme.Apply(AppTheme.Dark); shell.ThemeIconKey.ShouldBe("IconSun"); } }