using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Headless; using Avalonia.Input; using Avalonia.VisualTree; using DodoSSH.Client.App.Views; using DodoSSH.Client.Session; using DodoSSH.Client.Shell.ViewModels; using DodoSSH.Client.Ssh; using DodoSSH.Client.Storage; using DodoSSH.Client.Terminal; using NSubstitute; namespace DodoSSH.Client.App.Layout.Tests; /// /// How the rail's own switcher, mode-dependent first row and user popover answer a pointer. /// /// /// /// v5b moved three things onto this control that used to be tested elsewhere or not at all: the SSH/SFTP/S3 /// choice that used to be the tab strip's own fixed tabs (see TerminalTabsTests, which used to hold /// the equivalent of the first two facts below), the mode-dependent first row the design calls its own /// mode prop, and the popover that replaced the strip's vault menu. This suite is this control's /// counterpart to that one — a minimal shell with two tabs and no vault, for the reason /// TerminalTabsTests gives: nothing here reads Vault except the popover's vault switches, /// which this suite therefore does not open — that is VaultVisibilityTests' business, over the /// commands themselves, and this suite would only be re-testing the same command through a slower door. /// /// /// A UserControl in a bare window, for the same reason the palette's and the strip's suites are one: /// . /// /// public sealed class NavRailTests : IAsyncLifetime { private ClientCacheFactory caches = null!; private TerminalWorkspace workspace = null!; private MainWindowViewModel shell = null!; private static CancellationToken Token => TestContext.Current.CancellationToken; /// public ValueTask InitializeAsync() { caches = ClientCacheFactory.ForMemory($"navrail-{Guid.CreateVersion7():N}"); workspace = new TerminalWorkspace( new InMemoryTerminalAssetProvider(new Dictionary(StringComparer.Ordinal)), Substitute.For(), TimeProvider.System); shell = new MainWindowViewModel( ClientPaths.Default, caches, workspace, new VaultKnownHostStore(), Substitute.For(), (_, _) => throw new NotSupportedException("nothing here signs in"), TimeProvider.System, Substitute.For()) { State = ShellState.Unlocked, }; return ValueTask.CompletedTask; } /// public async ValueTask DisposeAsync() { await shell.DisposeAsync(); await workspace.DisposeAsync(); caches.Dispose(); } /// /// The switcher's three segments select the surface they name, and light up when it is the one showing. /// /// /// Driven through the segments rather than through the commands directly, for the reason /// TerminalTabsTests.TheFixedTabsSelectTheirSurface_AndVaultsRemembersItsPage gave for its own /// three fixed tabs: what is being checked is that three buttons in the markup are wired to three /// different things, which three commands called by hand would not catch if two of the three were /// bound to the same one. /// [Fact] public async Task TheSwitcherSegmentsSelectTheirSurface_AndLightTheActiveOne() { await OnTheRailAsync((rail, window) => { shell.IsSshShowing.ShouldBeTrue("nothing has navigated to SFTP or S3 yet"); Segment(rail, "SSH").Classes.Contains("active").ShouldBeTrue(); Click(Segment(rail, "SFTP"), window); shell.IsTransfersShowing.ShouldBeTrue(); Segment(rail, "SFTP").Classes.Contains("active").ShouldBeTrue(); Segment(rail, "SSH").Classes.Contains("active").ShouldBeFalse("exactly one segment lights at once"); Click(Segment(rail, "S3"), window); shell.IsBucketsShowing.ShouldBeTrue(); shell.IsTransfersShowing.ShouldBeFalse(); Segment(rail, "S3").Classes.Contains("active").ShouldBeTrue(); Click(Segment(rail, "SSH"), window); shell.IsSshShowing.ShouldBeTrue(); shell.IsBucketsShowing.ShouldBeFalse(); }); } /// /// The mode-dependent first row follows the same three flags the switcher above lights. /// /// /// The label is read straight off the row's own bound text rather than off /// state directly, because what a fidelity pass could break is the binding between the two, not the /// property computing the right string on its own — MainWindowViewModelTests would already catch /// that half. /// [Fact] public async Task TheFirstRailItemsLabel_FollowsTheSwitchersMode() { await OnTheRailAsync((rail, window) => { FirstRow(rail).ShouldBe("Terminal"); Click(Segment(rail, "SFTP"), window); FirstRow(rail).ShouldBe("Files"); Click(Segment(rail, "S3"), window); FirstRow(rail).ShouldBe("Buckets"); Click(Segment(rail, "SSH"), window); FirstRow(rail).ShouldBe("Terminal"); }); } /// /// The user popover is a Flyout, and this is the same assertion /// TerminalTabsTests.TheCaretBesideVaults_OpensTheVaultMenu made of the strip's own — the pointer /// opens the popup, and nothing about its position or its content occlude anything, since it never /// crosses into the terminal's own rectangle. See the remark in NavRail.axaml. /// [Fact] public async Task ClickingTheUserChip_OpensThePopover() { await OnTheRailAsync((rail, window) => { var chip = UserChip(rail); FlyoutBase.GetAttachedFlyout(chip)!.IsOpen.ShouldBeFalse("nothing has been pressed yet"); Click(chip, window); FlyoutBase.GetAttachedFlyout(chip)!.IsOpen.ShouldBeTrue(); }); } /// /// Each vault switch in the popover draws the vault's own display name and shown state, and is wired /// to the command that toggles it. /// /// /// /// is fed directly rather than through a real sign-in /// and a second vault created on a fake server. VaultVisibilityTests, in the Avalonia-free /// DodoSSH.Client.App.Tests project, already proves /// itself — that a hidden vault stays syncing, that the personal one refuses, and everything else the /// command actually does once it runs. What is worth proving here, in the project that can lay markup /// out at all, is only the wiring: that a row in this popover shows the right vault and calls that /// command with that vault when pressed — the fact a fidelity pass to this file could actually break. /// /// /// The command is read off the row rather than pressed, because /// itself declines with no observable effect when is null — which /// it is here, for the reason above — so a press would prove nothing a reader could tell from a press /// that reached the wrong command entirely. /// /// [Fact] public async Task PopoverVaultRows_NameTheirVaultAndAreWiredToToggleIt() { var shown = new VaultToggleViewModel(Guid.CreateVersion7(), "Personal", IsPersonal: true, IsShown: true); var hidden = new VaultToggleViewModel(Guid.CreateVersion7(), "Platform secrets", IsPersonal: false, IsShown: false); shell.VaultToggles.Add(shown); shell.VaultToggles.Add(hidden); await OnTheRailAsync((rail, window) => { Click(UserChip(rail), window); var shownRow = PopoverRow(window, shown); var hiddenRow = PopoverRow(window, hidden); shownRow.Command.ShouldBeSameAs(shell.ToggleVaultCommand); shownRow.CommandParameter.ShouldBeSameAs(shown); HasVisibleCheck(shownRow).ShouldBeTrue("the personal vault is always shown"); hiddenRow.Command.ShouldBeSameAs(shell.ToggleVaultCommand); hiddenRow.CommandParameter.ShouldBeSameAs(hidden); HasVisibleCheck(hiddenRow).ShouldBeFalse("this one was switched off"); }); } /// Settings, Vaults and Preferences each land on the screen they promise, and shut the popover. /// /// Three s over one private body rather than a : ShellScreen /// is internal, and a public theory method may not carry an internal type in its signature. /// [Fact] public Task ThePopoversSettingsRow_LandsOnPreferencesAndClosesThePopover() => APopoverRowLandsOnAsync("Settings", ShellScreen.Preferences); [Fact] public Task ThePopoversVaultsRow_LandsOnVaultsAndClosesThePopover() => APopoverRowLandsOnAsync("Vaults", ShellScreen.Vaults); [Fact] public Task ThePopoversPreferencesRow_LandsOnPreferencesAndClosesThePopover() => APopoverRowLandsOnAsync("Preferences", ShellScreen.Preferences); private Task APopoverRowLandsOnAsync(string label, ShellScreen target) => OnTheRailAsync((rail, window) => { var chip = UserChip(rail); Click(chip, window); Click(PopoverRow(window, label), window); shell.Screen.ShouldBe(target); shell.IsShowingPages.ShouldBeTrue(); FlyoutBase.GetAttachedFlyout(chip)!.IsOpen.ShouldBeFalse("a navigation row shuts the popover behind it"); }); /// /// Through Preferences rather than a direct SignOutCommand — see /// for why: the confirmation card the mock has no /// room for at all is drawn inline on that one screen while the vault is unlocked, and arming it from /// anywhere else would be a card raised nobody could see. /// [Fact] public async Task ThePopoversLogoutRow_GoesToPreferencesAndArmsTheSignOutConfirmation() { await OnTheRailAsync((rail, window) => { var chip = UserChip(rail); Click(chip, window); Click(PopoverRow(window, "Logout"), window); shell.Screen.ShouldBe(ShellScreen.Preferences); shell.IsConfirmingSignOut.ShouldBeTrue(); FlyoutBase.GetAttachedFlyout(chip)!.IsOpen.ShouldBeFalse(); }); } // ---- Helpers ---- private Task OnTheRailAsync(Action body) => LayoutHarness.OnTheUiThreadAsync( () => { var rail = new NavRail { DataContext = shell }; var window = new Window { Content = rail }; LayoutHarness.Settle(window, LayoutHarness.NavRailWidth, LayoutHarness.ScreenHeight); try { body(rail, window); } finally { window.Close(); } }, Token); private static Button Segment(Visual rail, string label) => rail.GetVisualDescendants() .OfType