Public Access
Choose more than one host card on the desktop, the way the phone already can
The chosen-hosts set has been in VaultViewModel since the phone's connect card became a contextual action bar: a set of entity ids, a tick on the row, and seven things that can be done to it. Only one head could fill it. The desktop's grid answered a press with one selection — the card the drawer, CONNECT and the context menu are about — so filing eleven imported machines under a heading was eleven drags, and clearing out a vault was eleven rounds of the deletion question. So the pointer gets three ways into the same set. Ctrl-clicks a card to tick it, Shift-clicks to tick the run between the anchor and the card, and drags a band out over the space between and below the cards to tick everything it touches. Esc, CLEAR, a plain click on a card and a click on the empty space each drop it, and Ctrl+A takes every card being drawn — VisibleHosts, so with something in the find box that is the ones on screen and not the ones it is hiding, which is the version of that shortcut whose result can be checked before Delete is pressed. TWO SELECTIONS ON ONE SCREEN, AND KEEPING THEM FROM DISAGREEING IS MOST OF THE CHANGE. Ctrl and Shift are answered on the tunnel and marked handled, so the ListBox never moves its own mark onto the card: a Ctrl-click that also selected would light the card it had just unticked and open the drawer on a machine somebody is removing from a set. A plain press drops the set unless it lands on a ticked card, and that case is deferred to the release, because the press may be the start of a drag of all of it. After any ordinary click exactly one card is in play, which is what makes every command on the screen unambiguous again. The context menu is where the seven live, and it is one markup with two halves gated on IsChoosingHosts. Connect, Browse files and Edit… are drawn only for a single ticked host, as the phone's sheet collapses them and for the same reason; the other four read better for a count. A right click on a card outside the set drops the set first, so a Delete… about the card under the pointer can never be offered while six sit ticked behind the menu — the same rule OnContextRequested has always enforced for the selection, reached from the other direction. No bar of buttons: the phone raises one because it has no other way to hold seven entries, and a strip repeating a menu that already exists would be a second home for the wording that matters most. What the desktop gains instead is a count beside the HOSTS heading, CLEAR, and a sentence saying where the actions are. A drag that starts on a ticked card carries every ticked card. The payload is a list rather than a row now, and a drop of more than one goes through FileChosenHostsUnder, which makes the refusals once — an open editor, and a group belonging to one keychain — and reports a count instead of forty status lines. Moving whichever card the pointer happened to be holding and leaving the other five where they are is a gesture that quietly does a fraction of what it looks like it does, and the five left behind look filed. The three panels the set's actions raise had never been drawn in a window: the vault picker with its key question, the group picker, and the deletion question. All three sit above the grid rather than over it, which is the arrangement the GROUPS section and the phone's list already use and for the reason written there — the ticked cards are the information the question exists to give, so the grid shortens instead. A DEFECT FOUND BEHIND IT, AND IT WAS ALREADY LIVE ON THE PHONE. The deletion question names a count and the run that answers it reads the set again, and nothing kept the two the same set: the panel is deliberately above a live list, so one more tick between "Delete these 6 hosts?" and pressing DELETE deleted seven, with the seventh named in nothing the user had read. It needed a deliberate act on a phone and a second's work with a band, which is what turned it up. VaultViewModel now remembers which hosts the question was asked about and drops the question when the set stops being them — the question rather than the set, because what somebody has just chosen is what they meant. It also covers the case nobody performs: a colleague's deletion arriving mid-question and shrinking the set under it. VERIFIED. 354 tests in App.Tests and 111 in App.Layout.Tests, build clean, no new warnings. Six gesture tests drive real pointer and key input through the headless window — the modifier click and what it must not do to the selection, the run and its re-measurement from the anchor, the band and the click that drops the set, Ctrl+A under a filter, and the menu's two halves — plus a DragOver carrying two hosts. Four layout tests measure the strip and the three panels at the window's minimum; the vault panel binds a key to its host first, or it would measure the short shape and certify the tall one. Two flow tests cover the multi-drop's write and its refusal, and the deletion question dropping itself. manual-checks gains 7.6a for dragging a set, which no test can see for the reason 7.6 gives, and 7.7a for the gestures — the rectangle actually being painted and the tick and the fill being legible together are the two things the harness cannot look at.
This commit is contained in:
@@ -119,7 +119,12 @@ public sealed class HostGridTests : IAsyncLifetime
|
||||
// The commands are the vault's, which is the other half of putting the menu on the list rather
|
||||
// than in the item template: a menu inside the template would have the row for its data context,
|
||||
// and every one of these would silently bind to nothing.
|
||||
var edit = menu.Items.OfType<MenuItem>().Single(item => item.Header is "Edit…");
|
||||
//
|
||||
// Drawn rather than merely present, because this menu carries a second Edit… that is about the
|
||||
// ticked set rather than about the selection; with nothing ticked that one is collapsed. See
|
||||
// TheMenuIsAboutTheSetWhileOneIsUpAndAboutTheCardOtherwise.
|
||||
var edit = menu.Items.OfType<MenuItem>()
|
||||
.Single(item => item.IsVisible && item.Header is "Edit…");
|
||||
edit.Command.ShouldBeSameAs(vault.EditSelectedHostCommand);
|
||||
|
||||
edit.Command!.Execute(null);
|
||||
@@ -260,7 +265,7 @@ public sealed class HostGridTests : IAsyncLifetime
|
||||
await OnTheGridAsync((screen, _) =>
|
||||
{
|
||||
var carried = new DataTransfer();
|
||||
carried.Add(DataTransferItem.Create(HostFormat, Row(vault, "prod-db")));
|
||||
carried.Add(DataTransferItem.Create(HostFormat, (IReadOnlyList<HostRowViewModel>)[Row(vault, "prod-db")]));
|
||||
|
||||
var onto = screen.GroupGrid
|
||||
.GetVisualDescendants()
|
||||
@@ -574,6 +579,245 @@ public sealed class HostGridTests : IAsyncLifetime
|
||||
});
|
||||
}
|
||||
|
||||
// ---- ◆ Choosing more than one card ----
|
||||
//
|
||||
// The set is the phone's — the same ids, the same seven actions, the same tick on the row — built here
|
||||
// with a pointer instead of a long press. What these hold is the half that belongs to this head: which
|
||||
// gesture means what, and the rule that keeps the list's own selection and the set from ever both being
|
||||
// about something at the same moment. See HostsScreen.axaml.cs.
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The modifier click, and the assertion that matters is the one about the selection: a Ctrl-click that
|
||||
/// also moved the <c>ListBox</c>'s own mark would light the card it had just unticked and open the drawer
|
||||
/// on a machine somebody is removing from a set. Stopping that is why the press is handled on the way
|
||||
/// down rather than acted on as it bubbles.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// And a plain click is the way out, which is the other half of the same rule: after one, exactly one
|
||||
/// card is in play and every command on this screen is about the same host.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task CtrlClickingCardsTicksThemWithoutMovingTheSelection()
|
||||
{
|
||||
await OnTheGridAsync((screen, window) =>
|
||||
{
|
||||
var first = Row(vault, "prod-db");
|
||||
var second = Row(vault, "stage-web");
|
||||
|
||||
vault.SelectedHost = first;
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
|
||||
Click(CardFor(screen, second), window, RawInputModifiers.Control);
|
||||
|
||||
vault.ChosenHostCount.ShouldBe(1);
|
||||
second.IsChosen.ShouldBeTrue("the tick is drawn on the card");
|
||||
vault.SelectedHost.ShouldBeSameAs(first, "ticking a card is not selecting it");
|
||||
screen.HostGrid.SelectedItem.ShouldBeSameAs(first, "and the list was never told otherwise");
|
||||
|
||||
Click(CardFor(screen, first), window, RawInputModifiers.Control);
|
||||
vault.ChosenHostCount.ShouldBe(2);
|
||||
|
||||
Click(CardFor(screen, first), window, RawInputModifiers.Control);
|
||||
vault.ChosenHostCount.ShouldBe(1, "the same click again takes the tick off");
|
||||
|
||||
Click(CardFor(screen, first), window);
|
||||
|
||||
vault.IsChoosingHosts.ShouldBeFalse("a plain click drops the set");
|
||||
vault.SelectedHost.ShouldBeSameAs(first, "and selects the card it landed on, as it always has");
|
||||
});
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The run is measured from the anchor every time rather than added to, which is what makes a Shift-click
|
||||
/// that overshot recoverable by clicking nearer — the behaviour every list of this kind has. The order is
|
||||
/// the grid's own, so "between" means between as the cards are laid out.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ShiftClickingTicksTheRunBetweenTheTwoCards()
|
||||
{
|
||||
await AddHostAsync("dev-box");
|
||||
|
||||
await OnTheGridAsync((screen, window) =>
|
||||
{
|
||||
var order = vault.VisibleHosts.ToList();
|
||||
|
||||
order.Count.ShouldBe(3, "three cards, so a run can have something in the middle of it");
|
||||
|
||||
Click(CardFor(screen, order[0]), window);
|
||||
Click(CardFor(screen, order[2]), window, RawInputModifiers.Shift);
|
||||
|
||||
vault.ChosenHostCount.ShouldBe(3);
|
||||
order.ShouldAllBe(row => row.IsChosen);
|
||||
|
||||
Click(CardFor(screen, order[1]), window, RawInputModifiers.Shift);
|
||||
|
||||
vault.ChosenHostCount.ShouldBe(2);
|
||||
order[2].IsChosen.ShouldBeFalse("the run is re-measured from the anchor, not extended");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The band, dragged out over the space around the cards.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// It starts below the cards rather than on one, which is the whole rule: a press on a card is a
|
||||
/// selection or the start of a drag of that host, and the band is what the space between and below them
|
||||
/// is for. The rectangle ticks what it touches rather than what it swallows — a card is 232 pixels wide,
|
||||
/// and a band that had to contain one would tick nothing at all when drawn down a column.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The second half is the same press without the drag: a click on the empty space is how somebody who
|
||||
/// never finds Esc gets out of a selection.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ABandDraggedOverTheCardsTicksThemAndAClickOnNothingDropsThem()
|
||||
{
|
||||
await OnTheGridAsync((screen, window) =>
|
||||
{
|
||||
var first = CardFor(screen, Row(vault, "prod-db"));
|
||||
var last = CardFor(screen, Row(vault, "stage-web"));
|
||||
|
||||
var topLeft = Corner(first, window);
|
||||
var bottomRight = Corner(last, window)
|
||||
+ new Point(last.Bounds.Width, last.Bounds.Height);
|
||||
|
||||
// Below every card, so the press lands on the scroller rather than on a list item.
|
||||
var from = bottomRight.WithY(bottomRight.Y + 24);
|
||||
var to = topLeft + new Point(2, 2);
|
||||
|
||||
// The button has to be named on the moves as well as on the press: a headless move carries the
|
||||
// button state in its modifiers, and one sent without it is the pointer being let go of.
|
||||
window.MouseDown(from, MouseButton.Left);
|
||||
window.MouseMove(new Point(to.X, from.Y), RawInputModifiers.LeftMouseButton);
|
||||
window.MouseMove(to, RawInputModifiers.LeftMouseButton);
|
||||
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
|
||||
vault.ChosenHostCount.ShouldBe(2, "the band was over both cards");
|
||||
|
||||
window.MouseUp(to, MouseButton.Left);
|
||||
|
||||
vault.ChosenHostCount.ShouldBe(2, "and letting go keeps what it was over");
|
||||
|
||||
window.MouseDown(from, MouseButton.Left);
|
||||
window.MouseUp(from, MouseButton.Left);
|
||||
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
|
||||
vault.IsChoosingHosts.ShouldBeFalse("a press and a release with no band between them is a click");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One menu with two halves, and which half is drawn is whether anything is ticked.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the multi-card version of the mistake the first test in this file exists for. The entries that
|
||||
/// act on the vault's selection and the entries that act on the set are in one markup, so the thing that
|
||||
/// must never happen is both being offered at once: a Delete… asking about the card under the pointer
|
||||
/// while six sit ticked behind the menu is the wrong machine deleted, arrived at from the other
|
||||
/// direction. A right click outside the set is what drops it, so the two are never both meaningful.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheMenuIsAboutTheSetWhileOneIsUpAndAboutTheCardOtherwise()
|
||||
{
|
||||
await OnTheGridAsync((screen, window) =>
|
||||
{
|
||||
var ticked = Row(vault, "prod-db");
|
||||
var other = Row(vault, "stage-web");
|
||||
|
||||
Click(CardFor(screen, ticked), window, RawInputModifiers.Control);
|
||||
|
||||
RightClick(CardFor(screen, ticked), window);
|
||||
|
||||
var menu = screen.HostGrid.ContextMenu.ShouldNotBeNull();
|
||||
|
||||
menu.IsOpen.ShouldBeTrue();
|
||||
vault.ChosenHostCount.ShouldBe(1, "a right click inside the set leaves it alone");
|
||||
|
||||
var offered = menu.Items.OfType<MenuItem>().Where(item => item.IsVisible).ToList();
|
||||
|
||||
offered.ShouldContain(item => ReferenceEquals(item.Command, vault.DeleteChosenHostsCommand));
|
||||
offered.ShouldNotContain(
|
||||
item => ReferenceEquals(item.Command, vault.DeleteHostCommand),
|
||||
"the entries about the selection are not offered beside the entries about the set");
|
||||
|
||||
menu.Close();
|
||||
|
||||
RightClick(CardFor(screen, other), window);
|
||||
|
||||
vault.IsChoosingHosts.ShouldBeFalse("a right click on a card outside the set drops it");
|
||||
vault.SelectedHost.ShouldBeSameAs(other, "and aims the ordinary menu, as it always has");
|
||||
|
||||
menu.Items.OfType<MenuItem>()
|
||||
.Where(item => item.IsVisible)
|
||||
.ShouldContain(item => ReferenceEquals(item.Command, vault.DeleteHostCommand));
|
||||
});
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Ctrl+A is every card <em>on the screen</em> and not every host in the keychain, which is the
|
||||
/// difference that matters the moment there is something in the find box: a shortcut that quietly ticked
|
||||
/// the machines it is not showing would be the worst possible input to Delete. Esc is the way back out,
|
||||
/// and the grid is where both are handled — Ctrl+A in the find box above has to go on selecting text.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task CtrlAChoosesEveryCardOnTheScreenAndEscapeDropsThem()
|
||||
{
|
||||
await AddHostAsync("dev-box");
|
||||
|
||||
vault.HostFilter = "prod";
|
||||
|
||||
await OnTheGridAsync((screen, window) =>
|
||||
{
|
||||
vault.VisibleHosts.Count.ShouldBe(1, "the filter is what makes this test about the screen");
|
||||
|
||||
screen.HostGrid.Focus();
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
|
||||
screen.HostGrid.IsFocused.ShouldBeTrue("the keys are the grid's");
|
||||
|
||||
window.KeyPressQwerty(PhysicalKey.A, RawInputModifiers.Control);
|
||||
|
||||
vault.ChosenHostCount.ShouldBe(1, "the one card being drawn, not the three hosts there are");
|
||||
|
||||
window.KeyPressQwerty(PhysicalKey.Escape, RawInputModifiers.None);
|
||||
|
||||
vault.IsChoosingHosts.ShouldBeFalse();
|
||||
});
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The drag carries a list because a drag that starts on a ticked card carries every ticked card, and the
|
||||
/// group cards have to answer for the whole of it. Refused only when there is nothing in it left to file:
|
||||
/// a set with one host from somewhere else in it is a real move, and offering the "no" cursor for it
|
||||
/// would be a drop the user can see the point of and cannot make.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task AGroupCardTakesAWholeSetOfDraggedHosts()
|
||||
{
|
||||
await OnTheGridAsync((screen, _) =>
|
||||
{
|
||||
IReadOnlyList<HostRowViewModel> set = [Row(vault, "prod-db"), Row(vault, "stage-web")];
|
||||
|
||||
var carried = new DataTransfer();
|
||||
carried.Add(DataTransferItem.Create(HostFormat, set));
|
||||
|
||||
var onto = screen.GroupGrid
|
||||
.GetVisualDescendants()
|
||||
.OfType<ListBoxItem>()
|
||||
.Single(item => item.DataContext is HostGroupRowViewModel);
|
||||
|
||||
var over = Over(onto, carried);
|
||||
|
||||
over.DragEffects.ShouldBe(DragDropEffects.Move, "both of them can be filed there");
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
/// <summary>The same in-process format the screen's own drag carries.</summary>
|
||||
@@ -582,8 +826,8 @@ public sealed class HostGridTests : IAsyncLifetime
|
||||
/// the name and the type — and a test holding the screen's own field would go on passing if the screen
|
||||
/// started carrying something else under it.
|
||||
/// </remarks>
|
||||
private static readonly DataFormat<HostRowViewModel> HostFormat =
|
||||
DataFormat.CreateInProcessFormat<HostRowViewModel>("dodossh-host-row");
|
||||
private static readonly DataFormat<IReadOnlyList<HostRowViewModel>> HostFormat =
|
||||
DataFormat.CreateInProcessFormat<IReadOnlyList<HostRowViewModel>>("dodossh-host-rows");
|
||||
|
||||
/// <summary>Holds a dragged host over one control and returns what the screen said about it.</summary>
|
||||
/// <remarks>
|
||||
@@ -610,6 +854,26 @@ public sealed class HostGridTests : IAsyncLifetime
|
||||
((Window)window).MouseUp(at, MouseButton.Right);
|
||||
}
|
||||
|
||||
/// <summary>A press and a release on one card, with whatever was being held down at the time.</summary>
|
||||
/// <remarks>
|
||||
/// Both halves, because the two say different things here: the press is where a modifier is read and
|
||||
/// where the set is dropped, and the release is where a press on a ticked card that turned out not to be
|
||||
/// a drag collapses onto it.
|
||||
/// </remarks>
|
||||
private static void Click(Visual card, Window window, RawInputModifiers held = RawInputModifiers.None)
|
||||
{
|
||||
var at = Centre(card, window);
|
||||
|
||||
window.MouseDown(at, MouseButton.Left, held);
|
||||
window.MouseUp(at, MouseButton.Left, held);
|
||||
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
}
|
||||
|
||||
private static Point Corner(Visual control, Visual window) =>
|
||||
control.TranslatePoint(default, window)
|
||||
?? throw new InvalidOperationException("the control is not in this window's tree");
|
||||
|
||||
private Task OnTheGridAsync(Action<HostsScreen, Window> body) =>
|
||||
LayoutHarness.OnTheUiThreadAsync(
|
||||
() =>
|
||||
@@ -677,6 +941,18 @@ public sealed class HostGridTests : IAsyncLifetime
|
||||
await vault.SaveGroupCommand.ExecuteAsync(null);
|
||||
}
|
||||
|
||||
/// <summary>One more machine, the way somebody adds one: through the editor.</summary>
|
||||
private async Task AddHostAsync(string label)
|
||||
{
|
||||
vault.NewHostCommand.Execute(null);
|
||||
vault.EditorLabel = label;
|
||||
vault.EditorHostname = $"{label}.internal";
|
||||
vault.EditorUsername = "deploy";
|
||||
|
||||
await vault.SaveHostCommand.ExecuteAsync(null);
|
||||
await vault.LoadAsync(Token);
|
||||
}
|
||||
|
||||
/// <remarks>Two hosts and a group, so there is a heading in the list and a selection to move off.</remarks>
|
||||
private async Task SeedAsync()
|
||||
{
|
||||
|
||||
@@ -630,6 +630,115 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
||||
await MeasureHostsAsync(faults => faults.ShouldBeEmpty("with the group move panel up"));
|
||||
}
|
||||
|
||||
// ---- ◆ The hosts screen with a set of cards ticked ----
|
||||
//
|
||||
// Ctrl, Shift and a band put the phone's chosen-hosts set on this screen — see HostsScreen.axaml.cs — and
|
||||
// with it come one strip and three panels that had never been drawn in a window. All four sit between the
|
||||
// HOSTS heading and the grid, so every one of them shortens the grid rather than overflowing it; that is
|
||||
// the property these measure. The gestures themselves are HostGridTests'.
|
||||
|
||||
/// <remarks>
|
||||
/// The strip: a count, CLEAR, and the sentence saying the actions are on the menu — squeezed between the
|
||||
/// heading and the grid's own count on the same row. It is the one thing on this screen whose width is
|
||||
/// set by nothing but its text, so what is really being measured is that the sentence trims instead of
|
||||
/// running out over the number at the far end. Measured with the drawer open, which is the width at which
|
||||
/// it does not fit and has to.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheHostsScreenFitsWhileCardsAreTicked()
|
||||
{
|
||||
vault.ChooseHostCommand.Execute(vault.Hosts[0]);
|
||||
vault.ToggleHostChoiceCommand.Execute(vault.Hosts[1]);
|
||||
|
||||
vault.IsChoosingHosts.ShouldBeTrue("the strip is only drawn while something is ticked");
|
||||
|
||||
vault.OpenHostPaneCommand.Execute(vault.Hosts[0]);
|
||||
vault.IsDrawerOpen.ShouldBeTrue("the drawer is what takes the width away");
|
||||
|
||||
await MeasureHostsAsync(faults => faults.ShouldBeEmpty("with two cards ticked"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The tallest of the three panels: the vault picker with the key question under it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A heading, a picker, a wrapping paragraph naming everything that travels and everything that does not,
|
||||
/// a tick with a second wrapping sentence beside it, and two buttons — all above the group cards and the
|
||||
/// grid, which still have to fit under it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The host is given a key first, because the tick is drawn only for a move of exactly one host that has
|
||||
/// something to bring; without that this would measure the short shape and say the long one fits. The
|
||||
/// panel is opened by hand rather than through <c>MoveChosenHostsToVault</c> for the reason the group
|
||||
/// move test gives: this fixture's account holds one vault, and the command declines rather than open a
|
||||
/// picker with nothing in it.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheHostsScreenFitsWithTheChosenHostsVaultPanelOpen()
|
||||
{
|
||||
vault.SelectedHost = vault.Hosts[0];
|
||||
vault.EditSelectedHostCommand.Execute(null);
|
||||
|
||||
vault.EditorSelectedAuthentication = vault.EditorAuthenticationChoices
|
||||
.First(choice => choice.Kind is AuthenticationKind.SshKey);
|
||||
|
||||
await vault.SaveHostCommand.ExecuteAsync(null);
|
||||
|
||||
vault.ChooseHostCommand.Execute(vault.Hosts[0]);
|
||||
|
||||
vault.ChosenHostVaultChoices.Add(
|
||||
new VaultChoiceViewModel(Guid.CreateVersion7(), "Platform Engineering secrets", false));
|
||||
|
||||
vault.SelectedChosenHostVault = vault.ChosenHostVaultChoices[0];
|
||||
vault.IsSendingChosenHostsToAVault = true;
|
||||
|
||||
vault.HasAChosenBindingToBring
|
||||
.ShouldBeTrue("the key question is the part of this panel worth measuring");
|
||||
|
||||
await MeasureHostsAsync(faults => faults.ShouldBeEmpty("with the set's vault panel up"));
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The group picker over the set, which is the same panel the phone draws and the same write a set
|
||||
/// dragged onto a group card makes. Shorter than the vault panel above and drawn in the same place, so
|
||||
/// what this adds is the picker being filled from one keychain's groups rather than from nothing.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheHostsScreenFitsWithTheChosenHostsGroupPanelOpen()
|
||||
{
|
||||
await SeedGroupsAsync(3);
|
||||
|
||||
vault.ChooseHostCommand.Execute(vault.Hosts[0]);
|
||||
vault.ToggleHostChoiceCommand.Execute(vault.Hosts[1]);
|
||||
|
||||
vault.RegroupChosenHostsCommand.Execute(null);
|
||||
|
||||
vault.IsRegroupingChosenHosts.ShouldBeTrue(vault.Status);
|
||||
vault.ChosenHostGroupChoices.Count.ShouldBeGreaterThan(1, "no group, and the three seeded ones");
|
||||
|
||||
await MeasureHostsAsync(faults => faults.ShouldBeEmpty("with the set's group panel up"));
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// One question for the whole set, drawn by the same card the group deletion above uses. The count is
|
||||
/// what makes it a confirmation somebody reads rather than one they press past, and the consequence line
|
||||
/// wraps — which is the part a narrower column would push out of the window.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheHostsScreenFitsWhileTheChosenHostsAreBeingDeleted()
|
||||
{
|
||||
vault.ChooseHostCommand.Execute(vault.Hosts[0]);
|
||||
vault.ToggleHostChoiceCommand.Execute(vault.Hosts[1]);
|
||||
|
||||
vault.DeleteChosenHostsCommand.Execute(null);
|
||||
|
||||
vault.IsConfirmingChosenHostDeletion.ShouldBeTrue("the question has to be up for this to measure it");
|
||||
|
||||
await MeasureHostsAsync(faults => faults.ShouldBeEmpty("with the set's deletion question up"));
|
||||
}
|
||||
|
||||
// ---- The vault screen ----
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user