Files
DodoSSH/tests/DodoSSH.Client.App.Layout.Tests/LayoutHarnessTests.cs
T
jaap-jan 9a76eced14
ci / build and test (ubuntu) (pull_request) Canceled after 0s
ci / build (windows) (pull_request) Canceled after 0s
Give hosts and terminals their own screen, and the rest of the vault another
Rebuilds the client's shell from an imported design: a titlebar and nav rail
it draws itself, real multi-session tabs over the one WebView, a Ctrl+K host
search, and a vault screen that merges keys, passwords and pinned host keys
into one table. Hosts left the vault column for their own screen beside the
terminal, which is what the design asks for and turned out to be the better
split anyway.

Two screens the design shows have nothing behind them yet — file transfer
and teams — and say so plainly rather than rendering invented data; every
other gap between the design and this build is recorded in
docs/design-import-gaps.md.
2026-07-31 08:39:37 +02:00

193 lines
6.7 KiB
C#

using System.Runtime.InteropServices;
using Avalonia.Controls;
using DodoSSH.Client.App.Views;
namespace DodoSSH.Client.App.Layout.Tests;
/// <summary>
/// The harness measuring itself.
/// </summary>
/// <remarks>
/// A clipping detector that never fires is worse than no detector, because it reads as a guarantee. So the
/// deliberately-broken window below is the most important test in this file: it is the one that proves the
/// green ones mean something. This is the same practice §5 of the handoff describes — a test proves nothing
/// until it has been seen to fail — applied to the instrument rather than to the code.
/// </remarks>
public sealed class LayoutHarnessTests
{
private static CancellationToken Token => TestContext.Current.CancellationToken;
[Fact]
public async Task AWindowWithRoomToSpare_ReportsNothing()
{
await LayoutHarness.OnTheUiThreadAsync(
() =>
{
var window = LayoutHarness.HostAtMinimumSize(
new Button { Content = "Save" },
LayoutHarness.MinimumWidth,
LayoutHarness.MinimumHeight);
try
{
LayoutHarness.Unreachable(window).ShouldBeEmpty();
}
finally
{
window.Close();
}
},
Token);
}
[Fact]
public async Task AButtonPushedPastTheBottomEdge_IsReportedByName()
{
// The instrument's own calibration. A stack taller than its window is exactly the shape of the
// defect the vault column is one editor away from, and if this passes silently the harness is
// decoration.
await LayoutHarness.OnTheUiThreadAsync(
() =>
{
var stack = new StackPanel();
for (var i = 0; i < 8; i++)
{
stack.Children.Add(new Button { Content = i == 7 ? "Save" : $"filler {i}" });
}
var window = LayoutHarness.HostAtMinimumSize(stack, 300, 120);
try
{
var faults = LayoutHarness.Unreachable(window);
faults.ShouldNotBeEmpty();
faults.ShouldContain(fault => fault.Contains("'Save'", StringComparison.Ordinal));
}
finally
{
window.Close();
}
},
Token);
}
[Fact]
public async Task AListLongerThanItsViewport_IsNotAFault()
{
// The exemption that keeps this harness usable. Scrolling is how a list is supposed to handle more
// rows than fit; without this the host list would fail the moment it had content.
await LayoutHarness.OnTheUiThreadAsync(
() =>
{
var stack = new StackPanel();
for (var i = 0; i < 40; i++)
{
stack.Children.Add(new Button { Content = $"row {i}" });
}
var window = LayoutHarness.HostAtMinimumSize(
new ScrollViewer { Content = stack },
300,
120);
try
{
LayoutHarness.Unreachable(window).ShouldBeEmpty();
}
finally
{
window.Close();
}
},
Token);
}
[Fact]
public async Task TheWholeWindowsXamlParses()
{
// Constructing it runs InitializeComponent, so this is what catches malformed XAML, a style selector
// that no longer resolves or a converter reference that has gone stale. Cheap, and it covers the
// whole file including the setup cards no other test here touches.
//
// Constructed and never shown, deliberately — see WhyTheWindowItselfIsNeverShown.
await LayoutHarness.OnTheUiThreadAsync(
() =>
{
var window = new MainWindow();
try
{
window.Content.ShouldNotBeNull();
}
finally
{
window.Close();
}
},
Token);
}
[Fact]
public async Task WhyTheWindowItselfIsNeverShown()
{
// Measured, not assumed, and pinned here so nobody spends an afternoon rediscovering it.
//
// Showing MainWindow attaches the terminal's NativeWebView, whose Win32 adapter initialises WebView2
// on attach — and WebView2 refuses an MTA thread, which is exactly why Program.Main is [STAThread].
// A HeadlessUnitTestSession owns its dispatcher thread and does not offer an apartment choice, so
// the whole window cannot be laid out here at any size.
//
// That is the reason this harness measures the extracted controls rather than MainWindow: each of
// them is a part with a height budget to blow, and none of them has a native child window in it. If a
// future Avalonia makes the adapter lazy, this test starts failing and the harness can be widened.
await LayoutHarness.OnTheUiThreadAsync(
() =>
{
var window = new MainWindow();
try
{
var showing = Should.Throw<COMException>(() => LayoutHarness.Settle(
window,
LayoutHarness.MinimumWidth,
LayoutHarness.MinimumHeight));
// RPC_E_CHANGED_MODE. Asserted on the code rather than the message so a localised
// Windows does not break the build.
showing.HResult.ShouldBe(unchecked((int)0x80010106));
}
finally
{
window.Close();
}
},
Token);
}
[Fact]
public async Task TheHarnessMeasuresTheSizeTheWindowDeclares()
{
// Pins the two constants against the XAML. A harness measuring a size the window lets itself
// be dragged to something smaller would be certifying a size no user is held to.
await LayoutHarness.OnTheUiThreadAsync(
() =>
{
var window = new MainWindow();
try
{
window.MinWidth.ShouldBe(LayoutHarness.MinimumWidth);
window.MinHeight.ShouldBe(LayoutHarness.MinimumHeight);
}
finally
{
window.Close();
}
},
Token);
}
}