using Avalonia.Controls; using Avalonia.Input; using Avalonia.Threading; using DodoSSH.Client.App.Views; using DodoSSH.Client.Session; using DodoSSH.Client.Session.Tests; using DodoSSH.Client.Shell.ViewModels; using DodoSSH.Client.Ssh; using DodoSSH.Client.Storage; using DodoSSH.Client.Terminal; using DodoSSH.Client.Transfer; using DodoSSH.Crypto; using NSubstitute; namespace DodoSSH.Client.App.Layout.Tests; /// /// The keyboard on the cards a closed vault shows: where it is when the card appears, and what Enter does. /// /// /// /// Launch used to leave the keyboard nowhere. Focus only moved on the way out of an unlocked vault, so the /// passphrase somebody started typing the moment the window appeared went nowhere until they clicked the box, /// and Enter in the server address did nothing at all. /// /// /// The whole window, which says cannot be shown /// here: the terminal's WebView refuses this thread's apartment when it is attached. It is taken out of the /// tree before the window is shown, which is everything that test is about and nothing these are. /// /// public sealed class ClosedVaultFocusTests { private const string Passphrase = "a sufficiently long passphrase"; private static readonly Argon2Profile CheapProfile = Argon2Profile.FromStoredParameters(memoryKibibytes: 8 * 1024, passes: 1, parallelism: 1); private static CancellationToken Token => TestContext.Current.CancellationToken; [Fact] public async Task AFreshMachine_PutsTheKeyboardInTheServerAddressAndEnterSignsIn() { using var caches = ClientCacheFactory.ForMemory($"focus-{Guid.CreateVersion7():N}"); await caches.MigrateAsync(Token); await using var workspace = Workspace(); await using var shell = Shell(caches, workspace); await shell.StartAsync(Token); shell.State.ShouldBe(ShellState.NeedsServer); await LayoutHarness.OnTheUiThreadAsync( () => WithWindow(shell, window => { var box = window.FindControl("ServerUrlBox").ShouldNotBeNull(); window.FocusManager!.GetFocusedElement().ShouldBeSameAs(box); box.KeyBindings.ShouldContain(binding => binding.Gesture == new KeyGesture(Key.Enter) && binding.Command == shell.SignInCommand); }), Token); } [Fact] public async Task AnEnrolledMachine_PutsTheKeyboardInThePassphraseBox() { using var caches = ClientCacheFactory.ForMemory($"focus-{Guid.CreateVersion7():N}"); await caches.MigrateAsync(Token); await new AccountProvisioner(new FakeAccountServer(), new StubKeyBinding(), caches, TimeProvider.System, CheapProfile) .EnrollAsync("https://dodossh.example", Passphrase, "laptop", "Personal", Token); await using var workspace = Workspace(); await using var shell = Shell(caches, workspace); await shell.StartAsync(Token); shell.State.ShouldBe(ShellState.Locked); await LayoutHarness.OnTheUiThreadAsync( () => WithWindow(shell, window => window.FocusManager!.GetFocusedElement() .ShouldBeSameAs(window.FindControl("UnlockPane").ShouldNotBeNull().PassphraseBox)), Token); } private static TerminalWorkspace Workspace() => new( new InMemoryTerminalAssetProvider(new Dictionary(StringComparer.Ordinal)), Substitute.For(), TimeProvider.System); private static MainWindowViewModel Shell(ClientCacheFactory caches, TerminalWorkspace workspace) => new( new ClientPaths(Path.Combine(Path.GetTempPath(), $"dodossh-focus-{Guid.CreateVersion7():N}")), caches, workspace, new VaultKnownHostStore(), new UnavailableDeviceKeyStore(), static (_, _) => throw new InvalidOperationException("A layout test has no network."), TimeProvider.System, Substitute.For(), CheapProfile); private static void WithWindow(MainWindowViewModel shell, Action assert) { var window = new MainWindow { DataContext = shell }; if (window.FindControl("Terminal") is { Parent: Panel panel } web) { panel.Children.Remove(web); } try { LayoutHarness.Settle(window, LayoutHarness.MinimumWidth, LayoutHarness.MinimumHeight); Dispatcher.UIThread.RunJobs(); assert(window); } finally { window.Close(); } } }