Public Access
Give the application a settings area built from what really exists
This commit is contained in:
@@ -6,7 +6,6 @@ using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using DodoSSH.Client.App.Views;
|
||||
using DodoSSH.Client.Domain;
|
||||
using DodoSSH.Client.Import;
|
||||
using DodoSSH.Client.Session;
|
||||
using DodoSSH.Client.Session.Tests;
|
||||
using DodoSSH.Client.Shell.ViewModels;
|
||||
@@ -825,9 +824,10 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
||||
|
||||
transfers.ShowsNoBuckets.ShouldBeTrue("this vault has no buckets in it");
|
||||
|
||||
// The plain-screen budget, not the session shell's: MainWindow.axaml gives S3 the same TransfersScreen
|
||||
// control with no tab row, no header and no sidebar around it — see its own remark on why the S3
|
||||
// usage is "deliberately not given the session shell above."
|
||||
// The buckets budget, not the full session shell's: MainWindow.axaml gives S3 the same TransfersScreen
|
||||
// control inside a padded, bordered container but with no tab row, no header, no status bar and no
|
||||
// sidebar around it — see its own remark on why the S3 usage is "deliberately not given the full
|
||||
// session shell above."
|
||||
await MeasureBucketsAsync(faults => faults.ShouldBeEmpty("with nothing to open yet"));
|
||||
}
|
||||
|
||||
@@ -847,27 +847,9 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
||||
await MeasureTransfersAsync(faults => faults.ShouldBeEmpty("with a drop in progress"));
|
||||
}
|
||||
|
||||
// ---- The import screen ----
|
||||
|
||||
[Fact]
|
||||
public async Task TheImportScreenFitsBeforeAnythingHasBeenScanned()
|
||||
{
|
||||
await MeasureImportAsync(faults => faults.ShouldBeEmpty("the state it opens in"));
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The shape with something to decide about: a table of candidate hosts with tickboxes, a warning
|
||||
/// block above it, and a footer carrying the sentence that says key files are not read. That sentence
|
||||
/// is the one that must not be pushed off the bottom — it is the difference between an import somebody
|
||||
/// understands and one they think is broken.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheImportScreenFitsWithHostsToChooseFromAndWarnings()
|
||||
{
|
||||
await MeasureImportAsync(
|
||||
faults => faults.ShouldBeEmpty("with a scanned list"),
|
||||
await ScannedImportAsync());
|
||||
}
|
||||
// v5c-3: the import screen's own layout coverage moved to SettingsPagesLayoutTests — it is a settings
|
||||
// page now, drawn inside settings mode over the Preferences page rather than beside the ordinary nav
|
||||
// rail; see MainWindowViewModel.IsImportOpen and design-notes/v5c-fidelity-notes.md.
|
||||
|
||||
// ---- The host keys screen ----
|
||||
|
||||
@@ -1898,87 +1880,6 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
||||
},
|
||||
Token);
|
||||
|
||||
/// <summary>Lays the import screen out at the size it gets beside the nav rail.</summary>
|
||||
private Task MeasureImportAsync(
|
||||
Action<IReadOnlyList<string>> assert,
|
||||
ImportViewModel? import = null) =>
|
||||
LayoutHarness.OnTheUiThreadAsync(
|
||||
() =>
|
||||
{
|
||||
var screen = new ImportScreen
|
||||
{
|
||||
DataContext = import ?? new ImportViewModel(vault, new SshConfigLocator()),
|
||||
};
|
||||
|
||||
var window = LayoutHarness.HostAtMinimumSize(
|
||||
screen, LayoutHarness.ScreenWidth, LayoutHarness.ScreenHeight);
|
||||
|
||||
try
|
||||
{
|
||||
assert(LayoutHarness.Unreachable(window));
|
||||
}
|
||||
finally
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
},
|
||||
Token);
|
||||
|
||||
/// <summary>
|
||||
/// An import view model that has scanned a real file, so the table has rows in it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Through a temporary directory rather than by populating the rows directly, because the shape being
|
||||
/// measured is what the parser produces — an entry with two warnings under it is taller than one
|
||||
/// without, and inventing the rows would measure a layout nothing generates.
|
||||
/// </remarks>
|
||||
private async Task<ImportViewModel> ScannedImportAsync()
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(), $"dodossh-import-{Guid.CreateVersion7():N}");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(directory, "config"),
|
||||
"""
|
||||
Host *
|
||||
ServerAliveInterval 30
|
||||
|
||||
Host prod-db
|
||||
HostName database.production.internal
|
||||
User deploy
|
||||
Port 2222
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
|
||||
Host bastion-eu-west-1
|
||||
HostName bastion.eu-west-1.example.com
|
||||
User ops
|
||||
ProxyCommand nc %h %p
|
||||
Compression yes
|
||||
compression no
|
||||
|
||||
Match host anything
|
||||
User root
|
||||
""");
|
||||
|
||||
var import = new ImportViewModel(vault, new SshConfigLocator(directory));
|
||||
|
||||
// Awaited, not fired. ScanCommand reads a file, so executing without awaiting measures an empty
|
||||
// table — which is the other test.
|
||||
await import.ScanCommand.ExecuteAsync(null);
|
||||
|
||||
import.HasRows.ShouldBeTrue("the fixture has hosts in it");
|
||||
import.HasWarnings.ShouldBeTrue("the fixture has a Match block and a wildcard block");
|
||||
|
||||
return import;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Lays the host keys screen out at the size it gets beside the nav rail.</summary>
|
||||
private Task MeasurePinsAsync(
|
||||
Action<IReadOnlyList<string>> assert,
|
||||
@@ -2142,12 +2043,14 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
||||
},
|
||||
Token);
|
||||
|
||||
/// <summary>Lays the S3 usage of <c>TransfersScreen</c> out at the plain-screen budget it actually gets.</summary>
|
||||
/// <summary>Lays the S3 usage of <c>TransfersScreen</c> out at the budget it actually gets.</summary>
|
||||
/// <remarks>
|
||||
/// The same control as <see cref="MeasureTransfersAsync"/> measures, at a different width and height: S3
|
||||
/// is "deliberately not given the session shell" — see <c>MainWindow.axaml</c>'s own remark on why — so it
|
||||
/// is measured at <see cref="LayoutHarness.ScreenWidth"/>/<see cref="LayoutHarness.ScreenHeight"/> instead,
|
||||
/// the same budget every other full-bleed page gets.
|
||||
/// The same control as <see cref="MeasureTransfersAsync"/> measures, at a different width and height. v5c-3
|
||||
/// gives S3 the session shell's own 26px-padded, 1px-bordered LOOK with none of its machinery — see
|
||||
/// <c>MainWindow.axaml</c>'s own remark on why — so it is measured at
|
||||
/// <see cref="LayoutHarness.BucketsScreenWidth"/>/<see cref="LayoutHarness.BucketsScreenHeight"/>, which
|
||||
/// take that padding and border off the full-bleed budget every other page gets and stop there: no tab
|
||||
/// row, header, status bar or sidebar to subtract, unlike <see cref="LayoutHarness.SessionScreenWidth"/>.
|
||||
/// </remarks>
|
||||
private Task MeasureBucketsAsync(Action<IReadOnlyList<string>> assert) =>
|
||||
LayoutHarness.OnTheUiThreadAsync(
|
||||
@@ -2156,7 +2059,7 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
||||
var screen = new TransfersScreen { DataContext = transfers };
|
||||
|
||||
var window = LayoutHarness.HostAtMinimumSize(
|
||||
screen, LayoutHarness.ScreenWidth, LayoutHarness.ScreenHeight);
|
||||
screen, LayoutHarness.BucketsScreenWidth, LayoutHarness.BucketsScreenHeight);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -2190,123 +2093,12 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
||||
bytesPerSecond,
|
||||
failure)));
|
||||
|
||||
/// <summary>Lays the vaults screen out at the width it gets once the nav rail has taken its column.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Its right-hand column is the narrowest measured here: the window's minimum is 1081, the nav rail
|
||||
/// takes 255 and the vault list 268, leaving 558 for everything above — the same 558 as before v5b
|
||||
/// widened the rail, because the minimum grew by exactly what the rail did.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Every list is seeded, and seeded with the long rows rather than the convenient ones — see
|
||||
/// <see cref="StubTeamServer"/>. The two states that hide half the screen, the rename form and the
|
||||
/// confirmation, are measured in their own tests below rather than here, because a control that is
|
||||
/// collapsed when the window is laid out is a control this suite has not checked.
|
||||
/// </para>
|
||||
/// v5c-2: the old VaultsScreen this suite used to measure here is gone — Vaults is a settings page now,
|
||||
/// and its own layout coverage (populated lists, the rename and new-vault forms, the hand-over
|
||||
/// confirmation, the members panel) lives in <c>SettingsPagesLayoutTests</c> beside every other settings
|
||||
/// page's, measured against the settings content budget rather than this suite's full-chrome one.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public Task TheVaultsScreen_FitsWithEveryListPopulated() =>
|
||||
OnTheVaultsScreenAsync(
|
||||
vaults => { },
|
||||
window => LayoutHarness.Unreachable(window)
|
||||
.ShouldBeEmpty("the vaults screen with members and key holders"));
|
||||
|
||||
/// <remarks>
|
||||
/// The rename form is drawn in place, above the members list, and pushes everything below it down.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public Task TheVaultsScreen_FitsWhileRenamingAVault() =>
|
||||
OnTheVaultsScreenAsync(
|
||||
vaults => vaults.RenameVaultCommand.Execute(null),
|
||||
window => LayoutHarness.Unreachable(window)
|
||||
.ShouldBeEmpty("the vaults screen with the rename form open"));
|
||||
|
||||
/// <remarks>
|
||||
/// The name-a-vault form is in the left column under the vault list. Worth its own case because the
|
||||
/// column is 268 wide and the sentence under the field wraps.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public Task TheVaultsScreen_FitsWithTheNewVaultFormOpen() =>
|
||||
OnTheVaultsScreenAsync(
|
||||
vaults => vaults.NewVaultCommand.Execute(null),
|
||||
window => LayoutHarness.Unreachable(window)
|
||||
.ShouldBeEmpty("the vaults screen with the new-vault form open"));
|
||||
|
||||
/// <remarks>
|
||||
/// The armed confirmation carries two sentences of prose and replaces the header's buttons. It is the
|
||||
/// tallest thing that can appear above the members list, so it is the case most likely to push the
|
||||
/// key-holders list off the bottom.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public Task TheVaultsScreen_FitsWhileConfirmingAHandOver() =>
|
||||
OnTheVaultsScreenAsync(
|
||||
vaults =>
|
||||
{
|
||||
vaults.SelectedMember = vaults.Members.First(member => !member.IsSelf);
|
||||
vaults.HandOverCommand.Execute(null);
|
||||
},
|
||||
window => LayoutHarness.Unreachable(window)
|
||||
.ShouldBeEmpty("the vaults screen with the hand-over confirmation armed"));
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A real <c>VaultsViewModel</c> over this suite's own unlocked session and a stub server. Both halves
|
||||
/// are needed and they answer different questions: the vault list is the session's, and who is in each
|
||||
/// vault is the server's.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A shared vault is created into the session first, because a session that has only ever been unlocked
|
||||
/// offline holds one personal vault — and the personal vault draws none of what this screen is for. It
|
||||
/// is created through the real <c>CreateTeamVaultAsync</c> rather than poked into the cache, so the row
|
||||
/// being measured is one the application could actually produce.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Selected before the second load rather than after it, so the members read is the awaited one: a
|
||||
/// selection assignment starts a read nothing can wait for, and measuring a window while it was still
|
||||
/// in flight would certify a screen with empty lists.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private async Task OnTheVaultsScreenAsync(
|
||||
Action<VaultsViewModel> arrange,
|
||||
Action<Window> assert)
|
||||
{
|
||||
using var teamServer = new StubTeamServer();
|
||||
|
||||
await session.CreateTeamVaultAsync(
|
||||
teamServer.Teams, StubTeamServer.SharedTeamId, "Platform secrets", Token);
|
||||
|
||||
var vaults = new VaultsViewModel(() => teamServer, () => session);
|
||||
|
||||
await vaults.LoadAsync(Token);
|
||||
|
||||
vaults.SelectedVault = vaults.Vaults.First(row => row.IsShared);
|
||||
|
||||
await vaults.LoadAsync(Token);
|
||||
|
||||
vaults.Members.ShouldNotBeEmpty("there is nothing to measure otherwise");
|
||||
|
||||
await LayoutHarness.OnTheUiThreadAsync(
|
||||
() =>
|
||||
{
|
||||
arrange(vaults);
|
||||
|
||||
var screen = new VaultsScreen { DataContext = vaults };
|
||||
|
||||
var window = LayoutHarness.HostAtMinimumSize(
|
||||
screen, LayoutHarness.ScreenWidth, LayoutHarness.ScreenHeight);
|
||||
|
||||
try
|
||||
{
|
||||
assert(window);
|
||||
}
|
||||
finally
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
},
|
||||
Token);
|
||||
}
|
||||
|
||||
private Task MeasureVaultAsync(Action<IReadOnlyList<string>> assert) =>
|
||||
OnTheVaultAsync((_, window) => assert(LayoutHarness.Unreachable(window)));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user