Public Access
Move the SFTP and S3 connection into the right-hand pane
THE CONNECT BAR IS GONE. It was a 44-pixel strip across the top of the file screen holding a heading, a picker, a password box and a button — chrome spanning both panes in order to configure one of them, drawn at full width whether or not anything was ever going to be connected. Underneath it sat a pane that was empty for exactly the same reason, saying so in a sentence nobody had to be told twice. So the pane says it instead. Disconnected, the right-hand half is an invitation where the listing would be: what the screen is for, what pressing the button costs, and one thing to press. SELECT HOST opens the picker in place — the same combo, the same password box, the same CONNECT — and the sentence and the button go away with it, because by then they have been read or they have not. Two steps rather than a picker sitting open, and that is not decoration. The reason the pane is empty is the question, and a combo box in the middle of it is a form with no question above it. The two steps also keep the panel inside its budget, which is the pane's height less whatever the queue has taken — 268 pixels with three transfers on it. Neither shape reaches the floor, and the layout suite measures both. IsChoosingRemote is cleared by everything that changes what the picker would be picking: connecting, disconnecting, moving between the SFTP and S3 tabs, and losing the vault. The last two are the ones that would rot quietly — a picker surviving a hop to the other tab offers hosts on a screen showing buckets, and one surviving a lock offers a list that has just been emptied because its rows carried decrypted secrets. CANCEL takes the typed password with it, which is a secret nobody asked to keep. WHERE THE TWO CONNECTED FACTS WENT. The address chip and DISCONNECT are a strip of their own inside the remote pane, above the listing — not three more cells in the header beside UP, REFRESH and DELETE. That pane is 381 pixels wide at the window's minimum and a fourth control in that row would have pushed one of the three off the edge; the number is written into the markup so the next thing added to either row is measured against it rather than tried. The status line did not fit there either. What is left after a 170-pixel address and a DISCONNECT is about eighty pixels, which turns every sentence into its first word and an ellipsis, so while a session is open it is in the queue's own strip at the foot of the screen, which spans the window. The other half of the time it is inside the invitation, beside the button that provoked it. One home in each state rather than two homes in one and none in the other. The header label reads HOST or BUCKET now, which is the only thing on the screen naming the kind — the bar that printed SFTP or S3 is gone and the tab in the strip says it either way, whether or not this screen is showing. The opening status text was "Choose a host and connect to browse its files", which the invitation now says in a heading, a sentence and a button. It is "Nothing is open yet.": a state rather than an instruction. That string is shared with the phone, where it still reads correctly under the picker card that head shows directly. Desktop only, and the phone is unchanged rather than merely untouched. Its FilesScreen is one pane at a time, so the picker *is* what it shows before a connection exists; it binds none of the new members. Four tests. Three in the layout suite — the picker open over a full queue, which is the tall shape and the one that has to be measured with the panes at their least; the bucket picker, which is a row shorter because an object store has nothing to type; and a session open with a long enough address to prove the chip gives way before DISCONNECT does. The fourth is the picker's lifecycle in ShellFlowTests, over all four things that put it away. 302 tests pass across the two suites.
This commit is contained in:
@@ -903,6 +903,83 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
||||
await MeasureTransfersAsync(faults => faults.ShouldBeEmpty());
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// ◆ The tallest the invitation gets, and the shape the connect bar's removal has to survive. Everything
|
||||
/// that used to be a strip across the top of the screen is now stacked in the right-hand pane — a
|
||||
/// picker, a password box and two buttons, under a heading — and that pane is only as tall as whatever
|
||||
/// the queue leaves it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// So the queue is filled first, which is what takes it to its 196-pixel maximum and leaves the panes
|
||||
/// their least. Without those rows this measures the invitation with nearly twice the room it is
|
||||
/// guaranteed, which is the version of this test that would pass whatever was added to the panel.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// No host is selected on purpose: a picker with nothing chosen shows the password box, because a host
|
||||
/// that names neither a key nor a credential is one that will ask for a password. That is the shape with
|
||||
/// the extra row in it.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheTransfersScreenFitsWithTheHostPickerOpenOverAFullQueue()
|
||||
{
|
||||
Enqueue(TransferDirection.Download, "artefact.tar.gz", 402_653_184, 149_000_000,
|
||||
TransferState.Running, bytesPerSecond: 6_500_000);
|
||||
|
||||
Enqueue(TransferDirection.Upload, "site-backup-2026-07-30.sql.gz", 8_100_000_000, 3_200_000_000,
|
||||
TransferState.Cancelled);
|
||||
|
||||
Enqueue(TransferDirection.Upload, "deploy.sh", 4_096, 0, TransferState.Failed);
|
||||
|
||||
transfers.BeginChoosingRemoteCommand.Execute(null);
|
||||
|
||||
transfers.IsChoosingRemote.ShouldBeTrue();
|
||||
transfers.SelectedHostAsksForAPassword
|
||||
.ShouldBeTrue("the password box is what makes this the tall shape");
|
||||
|
||||
await MeasureTransfersAsync(faults => faults.ShouldBeEmpty("with the picker open"));
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The bucket half of the picker, which differs by one row: no password box, because an object store
|
||||
/// carries its keys in the vault and has nothing to type. Measured because the two are separate markup
|
||||
/// rather than one picker with its items swapped — see the note on <c>RemoteKind</c>.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheTransfersScreenFitsWithTheBucketPickerOpen()
|
||||
{
|
||||
transfers.Remote = RemoteKind.Bucket;
|
||||
transfers.BeginChoosingRemoteCommand.Execute(null);
|
||||
|
||||
transfers.ShowsBucketPicker.ShouldBeTrue();
|
||||
transfers.SelectedHostAsksForAPassword.ShouldBeFalse("a bucket never asks for one");
|
||||
|
||||
await MeasureTransfersAsync(faults => faults.ShouldBeEmpty("with the bucket picker open"));
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The state the screen is in once something is open, and the reason it is worth a test of its own is
|
||||
/// the strip the connect bar left behind: a chip naming the account and the endpoint, the status line,
|
||||
/// and DISCONNECT — inside a pane that is under 400 pixels wide at the window's minimum, above a row
|
||||
/// that already carries UP, REFRESH and DELETE.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The address is a long one deliberately. It is the part of that row with no fixed width, and a chip
|
||||
/// that grew to fit whatever it was given is how the button beside it goes off the edge.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheTransfersScreenFitsWithASessionOpen()
|
||||
{
|
||||
transfers.IsConnected = true;
|
||||
transfers.ConnectedTo = "deployment-service@releases.eu-west.internal.example:2222";
|
||||
transfers.Status = "Connected to releases-eu.";
|
||||
|
||||
await MeasureTransfersAsync(faults => faults.ShouldBeEmpty("with a session open"));
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The trust card covers the whole screen, and it is the one thing here a user cannot get past without
|
||||
/// pressing something — so a button of its own that fell outside the window would leave the screen
|
||||
|
||||
@@ -5088,6 +5088,67 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
shell.Transfers.RemoteEntries.Select(entry => entry.Name).ShouldBe(["notes.txt"]);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The picker that replaced the desktop's connect bar, and the four things that put it away again. It
|
||||
/// is a flag rather than a screen, so what is worth asserting is that it is never left open over a pane
|
||||
/// it no longer belongs to: connecting closes it, disconnecting closes it, moving between SFTP and S3
|
||||
/// closes it, and losing the vault closes it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The last two are the ones that would rot quietly. A picker surviving a hop to the other tab offers
|
||||
/// hosts on a screen showing buckets, and one surviving a lock offers a list that has just been emptied
|
||||
/// because its rows carried decrypted secrets.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheFilePickerIsPutAwayByEverythingThatChangesWhatItWouldBePicking()
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
|
||||
shell.Transfers.Attach(vault, knownHosts);
|
||||
shell.Transfers.IsChoosingRemote.ShouldBeFalse("the pane opens on its invitation");
|
||||
|
||||
// Connecting.
|
||||
shell.Transfers.BeginChoosingRemoteCommand.Execute(null);
|
||||
shell.Transfers.SelectedHost = shell.Transfers.Hosts[0];
|
||||
|
||||
await shell.Transfers.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
shell.Transfers.IsConnected.ShouldBeTrue(shell.Transfers.Status);
|
||||
shell.Transfers.IsChoosingRemote.ShouldBeFalse("what is open is the listing now");
|
||||
|
||||
// Disconnecting, which puts the pane back to the invitation rather than to the form.
|
||||
await shell.Transfers.DisconnectCommand.ExecuteAsync(null);
|
||||
|
||||
shell.Transfers.IsConnected.ShouldBeFalse(shell.Transfers.Status);
|
||||
shell.Transfers.IsChoosingRemote.ShouldBeFalse();
|
||||
|
||||
// Moving to the other destination, which has a different list behind it.
|
||||
shell.Transfers.BeginChoosingRemoteCommand.Execute(null);
|
||||
shell.ShowFilesCommand.Execute(RemoteKind.Bucket);
|
||||
|
||||
shell.Transfers.ShowsBucketPicker.ShouldBeTrue();
|
||||
shell.Transfers.IsChoosingRemote.ShouldBeFalse("that picker was offering hosts");
|
||||
|
||||
// Cancelling, which also takes the typed password with it.
|
||||
shell.Transfers.BeginChoosingRemoteCommand.Execute(null);
|
||||
shell.Transfers.TypedPassword = "hunter2";
|
||||
|
||||
shell.Transfers.CancelChoosingRemoteCommand.Execute(null);
|
||||
|
||||
shell.Transfers.IsChoosingRemote.ShouldBeFalse();
|
||||
shell.Transfers.TypedPassword.ShouldBeEmpty("a secret nobody asked to keep");
|
||||
|
||||
// And losing the vault, which empties both lists.
|
||||
shell.Transfers.BeginChoosingRemoteCommand.Execute(null);
|
||||
|
||||
await shell.LockCommand.ExecuteAsync(null);
|
||||
|
||||
shell.Transfers.Hosts.ShouldBeEmpty();
|
||||
shell.Transfers.IsChoosingRemote.ShouldBeFalse("a form offering a choice between nothing");
|
||||
}
|
||||
|
||||
private async Task UnlockedAsync()
|
||||
{
|
||||
await EnrolledAndConfirmedAsync();
|
||||
|
||||
Reference in New Issue
Block a user