Public Access
Give the window its v5b chrome and each session surface its own shell
This commit is contained in:
@@ -2789,6 +2789,125 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
vault.Status.ShouldContain("host");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// ── v5b ── The rail's own count chips, per category — Keychain.dc.html draws a right-aligned mono
|
||||
/// count beside every category row, and this pins that each one is the same number the category's own
|
||||
/// list already carries rather than a second, hand-kept tally that could drift from it.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheCategoryRailCountsMatchTheUnderlyingLists()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddKeyAsync(vault, "deploy");
|
||||
await AddKeyAsync(vault, "backup");
|
||||
await AddCredentialAsync(vault, "pg-primary", "s3cret");
|
||||
await AddTagAsync(vault, "production");
|
||||
|
||||
vault.Keys.Count.ShouldBe(2);
|
||||
vault.Credentials.Count.ShouldBe(1);
|
||||
vault.Tags.Count.ShouldBe(1);
|
||||
vault.ObjectStores.Count.ShouldBe(0);
|
||||
vault.TotalItemCount.ShouldBe(4, "ALL's own chip counts every kind, buckets and tags included");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// ── v5b ── The type glyph Keychain.dc.html draws beside every row's name. Pinned per kind, because a
|
||||
/// glyph that silently fell back to the same one for two kinds would make ALL unreadable at a glance —
|
||||
/// which is the whole reason the column exists.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task EachVaultItemKindCarriesItsOwnGlyph()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddKeyAsync(vault, "deploy");
|
||||
await AddCredentialAsync(vault, "pg-primary", "s3cret");
|
||||
await AddTagAsync(vault, "production");
|
||||
|
||||
vault.ShowSectionCommand.Execute(VaultSection.All);
|
||||
|
||||
var byKind = vault.VaultItems.ToDictionary(row => row.Kind, row => row.IconGlyph);
|
||||
|
||||
byKind[VaultItemKind.Key].ShouldNotBeNullOrEmpty();
|
||||
byKind[VaultItemKind.Credential].ShouldNotBeNullOrEmpty();
|
||||
byKind[VaultItemKind.Tag].ShouldNotBeNullOrEmpty();
|
||||
|
||||
new[] { byKind[VaultItemKind.Key], byKind[VaultItemKind.Credential], byKind[VaultItemKind.Tag] }
|
||||
.Distinct(StringComparer.Ordinal).Count()
|
||||
.ShouldBe(3, "three kinds on the same table read as three different glyphs");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// ── v5b ── The filter box Keychain.dc.html adds to the table's own sub-toolbar — this table never had
|
||||
/// one before. It has to narrow and nothing else: a row it hides is still in the underlying list, and
|
||||
/// clearing it brings every row straight back.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheItemFilterNarrowsTheMergedTableByNameOrType()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddKeyAsync(vault, "deploy-key");
|
||||
await AddCredentialAsync(vault, "pg-primary", "s3cret");
|
||||
vault.ShowSectionCommand.Execute(VaultSection.All);
|
||||
|
||||
vault.ItemFilter = "deploy";
|
||||
|
||||
vault.VaultItems.Select(row => row.Name).ShouldBe(["deploy-key"]);
|
||||
|
||||
vault.ItemFilter = "PASSWORD";
|
||||
|
||||
vault.VaultItems.Select(row => row.Name).ShouldBe(
|
||||
["pg-primary"], "the type word matches too, case-insensitively");
|
||||
|
||||
vault.ItemFilter = string.Empty;
|
||||
|
||||
vault.VaultItems.Count.ShouldBe(2, "clearing the filter is not a second deletion");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// ── v5b ── The detail pane's own USED BY list and "in use" chip, and the table's USED BY column — all
|
||||
/// three read the same resolved-binding scan <c>HostsBoundTo</c> already used for the deletion warning,
|
||||
/// so this pins that a host genuinely bound to a key shows up in all three rather than in only one of
|
||||
/// them going stale relative to the others.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task AKeyBoundToAHost_ShowsUpInTheUsedByFactsEverywhereTheyAreDrawn()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddHostAsync(vault, "prod-api-01");
|
||||
await AddKeyAsync(vault, "deploy");
|
||||
|
||||
var key = vault.Keys[0];
|
||||
await BindKeyAsync(vault, Host(vault, "prod-api-01"), key.EntityId);
|
||||
|
||||
vault.ShowSectionCommand.Execute(VaultSection.All);
|
||||
var row = vault.VaultItems.Single(item => item.Kind is VaultItemKind.Key);
|
||||
|
||||
row.UsedBySummary.ShouldBe("prod-api-01");
|
||||
row.HasUsedBySummary.ShouldBeTrue();
|
||||
|
||||
vault.SelectedVaultItem = row;
|
||||
|
||||
vault.HasSelectedItemUsedByHosts.ShouldBeTrue();
|
||||
vault.SelectedItemUsedByHosts.ShouldHaveSingleItem().Label.ShouldBe("prod-api-01");
|
||||
vault.SelectedItemInUseSummary.ShouldBe("in use · 1 host");
|
||||
|
||||
// And a key nothing authenticates with says none of this — never a zero-count claim.
|
||||
await AddKeyAsync(vault, "unused");
|
||||
vault.SelectedVaultItem = vault.VaultItems.Single(
|
||||
item => string.Equals(item.Name, "unused", StringComparison.Ordinal));
|
||||
|
||||
vault.HasSelectedItemUsedByHosts.ShouldBeFalse();
|
||||
vault.SelectedItemInUseSummary.ShouldBe(string.Empty);
|
||||
}
|
||||
|
||||
// ---- Keeping an open editor's pickers in step with the vault ----
|
||||
|
||||
/// <remarks>
|
||||
@@ -5639,7 +5758,7 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
shell.Transfers.Status.ShouldContain("password");
|
||||
}
|
||||
|
||||
// ---- The terminal pin strip ----
|
||||
// ---- The v5b session sidebar's QUICK ACCESS ----
|
||||
|
||||
/// <summary>
|
||||
/// Sets up a host with one bound key and one pin, and connects a terminal to it. Returns the vault, with
|
||||
@@ -5666,16 +5785,16 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ThePinStrip_ShowsTheConnectedTabsHostsPins()
|
||||
public async Task TheSidebar_ShowsTheConnectedTabsHostsPins()
|
||||
{
|
||||
await ConnectedHostWithAPinAsync();
|
||||
|
||||
shell.ShowsPinStrip.ShouldBeTrue();
|
||||
shell.ShowsQuickAccessSidebar.ShouldBeTrue();
|
||||
shell.ActiveTabPinnedPaths.ShouldBe(["/var/www/app"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ThePinStrip_StaysHiddenBeforeAnythingConnects()
|
||||
public async Task TheSidebar_StaysHiddenBeforeAnythingConnects()
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
|
||||
@@ -5684,14 +5803,20 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
vault.AddEditorPinCommand.Execute(null);
|
||||
await vault.SaveHostCommand.ExecuteAsync(null);
|
||||
|
||||
// Pinned, but nothing has dialled it yet — the strip is keyed to a connected tab, not to the host
|
||||
// that happens to be selected on the hosts screen.
|
||||
shell.ShowsPinStrip.ShouldBeFalse();
|
||||
// Pinned, but nothing has dialled it yet, and no tab exists for the sidebar to be about — it is keyed
|
||||
// to a selected tab, not to the host that happens to be selected on the hosts screen.
|
||||
shell.ShowsQuickAccessSidebar.ShouldBeFalse();
|
||||
shell.ActiveTabPinnedPaths.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// v5b widened the sidebar's own gate from "this host has pins" to "a session is in focus" — the sidebar
|
||||
/// draws QUICK ACCESS's own "+ Pin folder" row and, on the terminal surface, SNIPS, both worth showing on
|
||||
/// a host that has pinned nothing yet. So a connected tab with no pins now shows the sidebar with an empty
|
||||
/// QUICK ACCESS list rather than hiding it, which is the opposite of what the old pin strip did.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ThePinStrip_StaysHiddenForAHostWithNoPins()
|
||||
public async Task TheSidebar_ShowsWithAnEmptyQuickAccessForAHostWithNoPins()
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
|
||||
@@ -5703,28 +5828,45 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
vault.ChooseHostCommand.Execute(Host(vault, "prod-db"));
|
||||
await vault.ConnectToChosenHostCommand.ExecuteAsync(null);
|
||||
|
||||
shell.ShowsPinStrip.ShouldBeFalse("this host pins nothing");
|
||||
shell.ShowsQuickAccessSidebar.ShouldBeTrue("a session is open, even though this host pins nothing");
|
||||
shell.ActiveTabPinnedPaths.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ThePinStrip_HidesWhenTheSurfaceLeavesTheTerminal()
|
||||
public async Task TheSidebar_HidesWhenTheSurfaceLeavesTheTerminalOrSftp()
|
||||
{
|
||||
await ConnectedHostWithAPinAsync();
|
||||
|
||||
shell.ShowsPinStrip.ShouldBeTrue();
|
||||
shell.ShowsQuickAccessSidebar.ShouldBeTrue();
|
||||
|
||||
shell.ShowScreenCommand.Execute(ShellScreen.Preferences);
|
||||
|
||||
shell.ShowsPinStrip.ShouldBeFalse("a page is showing, not the terminal the strip sits above");
|
||||
shell.ShowsQuickAccessSidebar.ShouldBeFalse(
|
||||
"a page is showing, not the terminal or SFTP the sidebar sits beside");
|
||||
|
||||
shell.SelectTabCommand.Execute(shell.Tabs[0]);
|
||||
|
||||
shell.ShowsPinStrip.ShouldBeTrue("back on the terminal surface, with the same tab selected");
|
||||
shell.ShowsQuickAccessSidebar.ShouldBeTrue("back on the terminal surface, with the same tab selected");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The pin strip's click handler, exercised through the fake SFTP factory rather than mocked: the
|
||||
/// The other surface the sidebar draws on since v5b: SFTP, gated on <c>Transfers.IsConnected</c> rather
|
||||
/// than on a selected tab, since a session on that screen is its own connection — see
|
||||
/// <c>MainWindowViewModel.ShowsQuickAccessSidebar</c>.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheSidebar_ShowsOnTheSftpSurfaceOnceConnected()
|
||||
{
|
||||
await ConnectedHostWithAPinAsync();
|
||||
|
||||
await shell.OpenPinnedPathCommand.ExecuteAsync("/var/www/app");
|
||||
|
||||
shell.IsTransfersShowing.ShouldBeTrue();
|
||||
shell.ShowsQuickAccessSidebar.ShouldBeTrue("the SFTP surface has a connected host of its own now");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The sidebar's QUICK ACCESS click handler, exercised through the fake SFTP factory rather than mocked: the
|
||||
/// terminal connection and the SFTP one are both real <c>ISshConnectionFactory</c>/
|
||||
/// <c>ISftpSessionFactory</c> calls against <c>FakeSshConnectionFactory</c>, so this is proof the two
|
||||
/// really are the second authenticated connection the design docs say they are — SftpRequests gets an
|
||||
@@ -5746,6 +5888,129 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
vault.Hosts[0].IsConnected.ShouldBeTrue("the terminal session is untouched by opening a files pane");
|
||||
}
|
||||
|
||||
// ---- The v5b session shell: tab rows, header/status-bar facts, cross-surface buttons ----
|
||||
|
||||
/// <remarks>
|
||||
/// The SFTP tab row's click, resolved through the same "Browse files" plumbing a pin click already uses —
|
||||
/// see the deviation recorded on <c>MainWindowViewModel.SelectFilesHostCommand</c>.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task SelectingATabsFilesOpensSftpAtThatHostAndMarksTheTabSelected()
|
||||
{
|
||||
await ConnectedHostWithAPinAsync();
|
||||
|
||||
var tab = shell.Tabs[0];
|
||||
shell.SelectedTab = null;
|
||||
|
||||
await shell.SelectFilesHostCommand.ExecuteAsync(tab);
|
||||
|
||||
shell.SelectedTab.ShouldBe(tab, "the tab row's own active mark reads IsSelected");
|
||||
shell.IsTransfersShowing.ShouldBeTrue();
|
||||
shell.Transfers.SelectedHost.ShouldNotBeNull().Label.ShouldBe("prod-db");
|
||||
shell.Transfers.IsConnected.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The header's "Open terminal" button on the SFTP surface — the other half of the two cross-surface
|
||||
/// directions the v5b notes ask for, through <c>VaultViewModel.ConnectCommand</c> rather than through a
|
||||
/// tab that does not exist.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task OpeningATerminalFromSftpConnectsANewTerminalToTheBrowsedHost()
|
||||
{
|
||||
await ConnectedHostWithAPinAsync();
|
||||
await shell.OpenPinnedPathCommand.ExecuteAsync("/var/www/app");
|
||||
|
||||
shell.Transfers.IsConnected.ShouldBeTrue();
|
||||
var tabsBefore = shell.Tabs.Count;
|
||||
|
||||
await shell.OpenTerminalForFilesHostCommand.ExecuteAsync(null);
|
||||
|
||||
shell.Tabs.Count.ShouldBe(tabsBefore + 1, "a new terminal connected to the browsed host");
|
||||
shell.SelectedTab.ShouldNotBeNull().Label.ShouldBe("prod-db");
|
||||
shell.IsTerminalShowing.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The sidebar's SNIPS row, wired through <c>SnippetsViewModel.InsertCommand</c> rather than a second
|
||||
/// insert path — see the deviation recorded on <c>MainWindowViewModel.InsertSnippetCommand</c>. Proven
|
||||
/// through a real connected tab and a real renderer, the same fixture <c>InsertingASnippet_...</c> above
|
||||
/// uses for the standalone screen, because what is worth proving here is that the shell's command reaches
|
||||
/// that same mechanism rather than reimplementing it.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task InsertingASnippetFromTheSidebarTypesItIntoTheSelectedTab()
|
||||
{
|
||||
await ConnectedHostWithAPinAsync();
|
||||
|
||||
var snippets = shell.SnippetsScreen.ShouldNotBeNull();
|
||||
await AddSnippetAsync(snippets, "uptime", "uptime", runs: false);
|
||||
|
||||
var row = snippets.Visible.ShouldHaveSingleItem();
|
||||
|
||||
await shell.InsertSnippetCommand.ExecuteAsync(row);
|
||||
|
||||
snippets.Selected.ShouldBe(row, "the sidebar row picks the same selection INSERT reads");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddingASnipFromTheSidebar_OpensTheSnippetsScreenWithTheEditorOpen()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
|
||||
shell.AddSnippetFromSidebarCommand.Execute(null);
|
||||
|
||||
shell.IsSnippetsShowing.ShouldBeTrue();
|
||||
shell.SnippetsScreen.ShouldNotBeNull().IsEditing.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The closest honest affordance the v5b notes ask for: this application cannot open a host editor
|
||||
/// scrolled to one card, so "+ Pin folder" opens the whole editor on the active tab's host, the same as
|
||||
/// the hosts screen's own EDIT does.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task PinningAFolderFromTheSidebar_OpensTheActiveTabsHostEditor()
|
||||
{
|
||||
await ConnectedHostWithAPinAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
shell.PinFolderFromSidebarCommand.Execute(null);
|
||||
|
||||
shell.IsHostsShowing.ShouldBeTrue();
|
||||
vault.IsEditing.ShouldBeTrue();
|
||||
vault.SelectedHost.ShouldNotBeNull().Label.ShouldBe("prod-db");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The status bar's facts, read off the selected terminal tab. <see cref="MainWindowViewModel.SessionElapsedText"/>
|
||||
/// is real, not fabricated: <c>StartedAt</c> is set from the shell's own clock at the moment the session
|
||||
/// opens, and this reads it back through the same clock.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task SessionFacts_ReflectTheSelectedTerminalTab()
|
||||
{
|
||||
await ConnectedHostWithAPinAsync();
|
||||
|
||||
shell.IsSessionConnected.ShouldBeTrue();
|
||||
shell.SessionAddress.ShouldBe(shell.Tabs[0].Address);
|
||||
shell.SessionElapsedText.ShouldNotBeNull().ShouldStartWith("session ");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The honesty rule stated as a test: with nothing open, the status bar has no facts to show rather than
|
||||
/// a blank or a placeholder standing in for them.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task SessionFacts_AreAbsentWithNoSessionOpen()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
|
||||
shell.IsSessionConnected.ShouldBeFalse();
|
||||
shell.SessionAddress.ShouldBeNull();
|
||||
shell.SessionElapsedText.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AGroupsHeading_OpensThatGroupsEditorRatherThanAnotherOne()
|
||||
{
|
||||
@@ -6593,6 +6858,40 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
snippets.Status.ShouldContain("no longer connected", Case.Sensitive);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// ── v5b ── The desktop's own delete confirmation, additive beside the phone's uncounted DELETE — see
|
||||
/// <c>SnippetsViewModel.RequestDelete</c>'s own remarks for why the two are separate commands. Pins the
|
||||
/// two-step shape every other keychain item's deletion already has: arming changes nothing, and only
|
||||
/// confirming actually removes it.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task RequestingDeleteOnASnippet_AsksFirstAndChangesNothingUntilConfirmed()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var snippets = shell.SnippetsScreen.ShouldNotBeNull();
|
||||
|
||||
await AddSnippetAsync(snippets, "restart the api", "sudo systemctl restart dodossh-api", runs: false);
|
||||
|
||||
snippets.Selected = snippets.Visible.Single();
|
||||
snippets.RequestDeleteCommand.Execute(null);
|
||||
|
||||
snippets.IsConfirmingDelete.ShouldBeTrue();
|
||||
snippets.DeleteQuestion.ShouldContain("restart the api");
|
||||
snippets.ShowsSelectionActions.ShouldBeFalse("the confirm card takes the insert controls' place");
|
||||
snippets.Visible.ShouldHaveSingleItem("arming the question deletes nothing by itself");
|
||||
|
||||
snippets.CancelDeleteCommand.Execute(null);
|
||||
|
||||
snippets.IsConfirmingDelete.ShouldBeFalse();
|
||||
snippets.Visible.ShouldHaveSingleItem("cancelling leaves the snippet exactly where it was");
|
||||
|
||||
snippets.RequestDeleteCommand.Execute(null);
|
||||
await snippets.ConfirmDeleteCommand.ExecuteAsync(null);
|
||||
|
||||
snippets.IsConfirmingDelete.ShouldBeFalse();
|
||||
snippets.Visible.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
private static SnippetsViewModel SnippetsOver(
|
||||
VaultViewModel vault,
|
||||
InsertTarget target,
|
||||
@@ -6646,6 +6945,55 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
await vault.SaveHostCommand.ExecuteAsync(null);
|
||||
}
|
||||
|
||||
// ---- Logs ----
|
||||
|
||||
/// <remarks>
|
||||
/// ── v5b ── Logs.dc.html's own two-segment CONNECTIONS/KEYCHAIN control, restyled onto the rail's own
|
||||
/// track-and-segment idiom — see LogsScreen.axaml's own remark on why two segments and not three. Pins
|
||||
/// that <c>LogSection</c> genuinely has only the two values the segments name, and that the two flags a
|
||||
/// segment's own active state reads are mutually exclusive.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheLogsScreenSwitchesBetweenExactlyTheTwoRealSections()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var logs = shell.LogsScreen.ShouldNotBeNull();
|
||||
|
||||
logs.Section.ShouldBe(LogSection.Connections, "the screen opens on connections");
|
||||
logs.ShowsConnections.ShouldBeTrue();
|
||||
logs.ShowsActivity.ShouldBeFalse();
|
||||
|
||||
logs.ShowSectionCommand.Execute(LogSection.Activity);
|
||||
|
||||
logs.ShowsActivity.ShouldBeTrue();
|
||||
logs.ShowsConnections.ShouldBeFalse("the two flags are one fact read two ways and cannot both be true");
|
||||
|
||||
logs.ShowSectionCommand.Execute(LogSection.Connections);
|
||||
logs.ShowsConnections.ShouldBeTrue();
|
||||
|
||||
Enum.GetValues<LogSection>().Length.ShouldBe(2, "the design draws two segments and the enum has two");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// ── v5b ── The header's own status sentence — LogsViewModel.HeaderStatusLine — falls back to the per-
|
||||
/// section fact this type's header comment already states truthfully, and steps aside for a refresh
|
||||
/// error when refreshing just produced one.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheHeaderStatusLineNamesTheSectionsOwnFactAndYieldsToARefreshError()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var logs = shell.LogsScreen.ShouldNotBeNull();
|
||||
|
||||
logs.HeaderStatusLine.ShouldBe("an entry is written once, when a connection closes");
|
||||
|
||||
logs.ShowSectionCommand.Execute(LogSection.Activity);
|
||||
logs.HeaderStatusLine.ShouldBe("one row per write, per device");
|
||||
|
||||
logs.Status = "network unreachable";
|
||||
logs.HeaderStatusLine.ShouldBe("network unreachable", "a live refresh error outranks the section fact");
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
private static CancellationToken Token => TestContext.Current.CancellationToken;
|
||||
|
||||
@@ -639,7 +639,13 @@ public sealed class VaultVisibilityTests : IAsyncLifetime
|
||||
vault.IsEditing.ShouldBeFalse(vault.Status);
|
||||
}
|
||||
|
||||
/// <summary>Switches a vault off through the menu, as the tab strip does.</summary>
|
||||
/// <summary>
|
||||
/// Switches a vault off through <see cref="MainWindowViewModel.ToggleVaultCommand"/> — the command
|
||||
/// behind a vault row in the rail's own user popover now, not the tab strip's old vault menu, which is
|
||||
/// gone; see <c>NavRail.axaml</c>. This project is deliberately Avalonia-free, so what this file proves
|
||||
/// is the command's own effect; that the popover's row is wired to call it is
|
||||
/// <c>DodoSSH.Client.App.Layout.Tests.NavRailTests</c>' business.
|
||||
/// </summary>
|
||||
private async Task HideAsync(Guid vaultId)
|
||||
{
|
||||
var toggle = shell.VaultToggles.Single(row => row.VaultId == vaultId);
|
||||
|
||||
Reference in New Issue
Block a user