Merge branch 'claude/sftp-s3-connection-ui-b0730f'
ci / android head (push) Failing after 5s
ci / build and test (push) Successful in 1m23s
ci / api image (push) Successful in 24s

This commit is contained in:
2026-08-03 15:51:19 +02:00
5 changed files with 427 additions and 122 deletions
@@ -926,6 +926,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