Public Access
Let one card be selected at a time
The hosts screen draws two grids, one above the other, and each is a ListBox with a selection of its own. Nothing joined them, so a group card and a host card could be lit at the same moment — two chosen things, under two pairs of buttons of which only one would act on whichever the eye had settled on. Both grids mark a selection the same way, so there was nothing on screen to say which of the two the next press belonged to. They share one mark now. Selecting a host clears the group and selecting a group clears the host, and that second one takes the detail pane down with it: a pane about one machine cannot go on standing beside a marked group, because nothing on it would be about what is selected. Losing a selection deliberately clears nothing. A null arrives whenever either list is rebuilt — every keystroke in the filter box and every background sync — and treating that as somebody deselecting would take the mark off a group card because a search emptied the grid beneath it. The reload needed the same guard for the same reason. It falls back to the first host when nothing is selected, which is what puts a target under CONNECT on a fresh unlock; with one mark between the two grids that fallback would have unselected a group nobody had touched, once a minute. It is skipped while a group holds the selection, and it moved into a method of its own because the comment saying why pushed ReloadHostsAsync past sixty lines. One consequence needed handling rather than accepting. The phone's only route into the group editor is a button on a heading in the host list, and it worked by selecting the group first — which under this rule takes the highlight off the machine somebody was about to connect to, on a screen that draws no group cards to say where it has gone. EditGroup takes the group as an argument now: the heading passes its own row, and the desktop's button beside the cards passes nothing and still means "the card that is selected". What the tests hold is the half that lives in the controls. Clearing the property has to reach the list that is drawing the card, and a selection nulled in the view model while the card stays highlighted is the exact failure this is about — so the rule is driven on the real screen, in both directions, against the ListBoxes' own SelectedItem. The desktop's EDIT is pressed through its binding for the same kind of reason: a command refusing the button's empty parameter would be a button that never fires, and nothing about the markup would say so.
This commit is contained in:
@@ -259,6 +259,99 @@ public sealed class HostGridTests : IAsyncLifetime
|
||||
vault.ShowsGroupActions.ShouldBeFalse("a pair of buttons with no subject is hidden rather than shown");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The two grids share one selection, so at most one card on the screen is ever lit.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// They are two <c>ListBox</c>es, each holding a selection of its own and each drawing it the same way.
|
||||
/// Left to themselves both stay marked — a group above and a host below — under two pairs of buttons of
|
||||
/// which only one acts on whichever card the eye has settled on. The vault is what joins them.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Driven on the screen rather than on the view model alone, because half of the rule lives in the
|
||||
/// controls: clearing the property has to reach the list that is drawing the card, and a selection
|
||||
/// nulled in the view model while the card stays highlighted is the exact failure this is about.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheHostAndGroupGridsShareOneSelection()
|
||||
{
|
||||
await OnTheGridAsync((screen, _) =>
|
||||
{
|
||||
var host = Row(vault, "stage-web");
|
||||
|
||||
vault.OpenHostPaneCommand.Execute(host);
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
|
||||
vault.SelectedGroup = vault.VisibleGroups.Single();
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
|
||||
vault.SelectedHost.ShouldBeNull("choosing a group is choosing something else");
|
||||
vault.SelectedSidebarRow.ShouldBeNull("and the list that draws the hosts is told");
|
||||
screen.HostGrid.SelectedItem.ShouldBeNull();
|
||||
CardFor(screen, host).IsSelected.ShouldBeFalse("the card the pointer left has to go dark");
|
||||
vault.IsDrawerOpen.ShouldBeFalse("a pane about one host cannot stand beside a marked group");
|
||||
|
||||
vault.SelectedHost = host;
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
|
||||
vault.SelectedGroup.ShouldBeNull("and the same in the other direction");
|
||||
screen.GroupGrid.SelectedItem.ShouldBeNull();
|
||||
GroupCard(screen).IsSelected.ShouldBeFalse();
|
||||
vault.ShowsGroupActions.ShouldBeFalse("so the group's own two buttons have nothing to act on");
|
||||
});
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// EDIT takes the group as an argument now, so that the phone can open the editor on a heading without
|
||||
/// selecting a group and losing the host selection to it — see <c>VaultViewModel.EditGroup</c>. The
|
||||
/// button beside the cards passes nothing and means "the card that is selected", which is the half of
|
||||
/// that change that would fail silently: a command refusing a null parameter is a button that never
|
||||
/// fires, and nothing about the markup would say so.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheGroupsEditButtonStillActsOnTheSelectedCard()
|
||||
{
|
||||
await OnTheGridAsync((screen, _) =>
|
||||
{
|
||||
vault.SelectedGroup = vault.VisibleGroups.Single();
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
|
||||
var edit = screen.GetVisualDescendants()
|
||||
.OfType<Button>()
|
||||
.Single(button => ReferenceEquals(button.Command, vault.EditGroupCommand));
|
||||
|
||||
edit.IsEffectivelyEnabled.ShouldBeTrue("the command has to accept the button's empty parameter");
|
||||
|
||||
edit.Command.ShouldNotBeNull().Execute(edit.CommandParameter);
|
||||
|
||||
vault.IsEditingGroup.ShouldBeTrue();
|
||||
vault.GroupEditorLabel.ShouldBe("production", "the card that was selected");
|
||||
});
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The half of the shared selection that is nobody's gesture. A reload falls back to the first host when
|
||||
/// nothing is selected, which is what puts a target under CONNECT on a fresh unlock — and with one mark
|
||||
/// between the two grids that fallback would quietly unselect a group card every time a sync landed.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ASyncDoesNotTakeTheSelectionOffAGroupCard()
|
||||
{
|
||||
var production = vault.VisibleGroups.Single();
|
||||
|
||||
vault.SelectedGroup = production;
|
||||
vault.SelectedHost.ShouldBeNull("the seed's load left a host selected, and the group took the mark");
|
||||
|
||||
await vault.LoadAsync(Token);
|
||||
|
||||
vault.SelectedHost.ShouldBeNull("the reload invented none under the card that was chosen");
|
||||
vault.SelectedGroup
|
||||
.ShouldNotBeNull("re-found by id, since the reload replaces every row object in the list")
|
||||
.EntityId.ShouldBe(production.EntityId);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The gesture, performed. It is wired in the control rather than bound in the markup — which is exactly
|
||||
/// the sort of wiring that compiles whether or not it is connected to anything — and the first half of
|
||||
|
||||
@@ -4333,6 +4333,35 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
vault.GroupEditorLabel.ShouldBe("production", "the heading pressed, not the group selected");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The heading hands its group to the editor rather than selecting it first, and this is why. A group
|
||||
/// selection clears the host selection — the desktop's two grids share one mark — and the phone draws no
|
||||
/// group cards at all, so selecting one here would take the highlight off the machine in the list with
|
||||
/// nothing on screen to say where it had gone, or how to get it back.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task AGroupsHeading_LeavesTheChosenMachineChosen()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddHostAsync(vault, "prod-db");
|
||||
await AddGroupAsync(vault, "production");
|
||||
await FileAsync(vault, "prod-db", "production");
|
||||
|
||||
var host = vault.Hosts.Single();
|
||||
vault.SelectedHost = host;
|
||||
|
||||
var heading = vault.SidebarRows.OfType<SidebarGroupHeader>().Single(
|
||||
row => string.Equals(row.Label, "production", StringComparison.Ordinal));
|
||||
|
||||
vault.EditGroupFromHeadingCommand.Execute(heading);
|
||||
|
||||
vault.IsEditingGroup.ShouldBeTrue("the editor still opens on the group the heading names");
|
||||
vault.GroupEditorLabel.ShouldBe("production");
|
||||
vault.SelectedHost.ShouldBeSameAs(host, "and the list is still on the machine it was on");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheUngroupedHeading_OpensNothing()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user