Public Access
The pipeline finally reached the tests and found four failures. None was the pipeline's, and only one of the four was a test being fussy about a platform rather than telling the truth about one. The local pane's roots bar was the real bug. LocalDirectory.Roots built it from DriveInfo.GetDrives on every platform, and its own summary — "the drives on Windows, and the root elsewhere" — had been describing an intention rather than the code for as long as nobody ran it off Windows. On Unix that call answers with every mount the kernel holds: /proc, /sys/fs/bpf, one per installed snap, /run/user/1000/doc, some forty on an ordinary laptop. The transfers screen draws a button per root, so the bar ran to about five thousand pixels inside an eight-hundred pixel window. Anybody running the Linux build has been looking at that. Filtering GetDrives is not the fix and the comment now says why at length, because it is the obvious thing to try: DriveType answers Fixed for / and /home and equally for every squashfs snap, for efivarfs and for tracefs, while /boot/efi comes back Removable, and DriveFormat would need a hand-kept list of every virtual filesystem Linux might grow. So Unix now names what somebody would want instead of subtracting what they would not — the root, their home, and whatever is mounted under /run/media/<user>, /media, /mnt or /Volumes. Anything else is still reachable by navigating from /, which is what the pane is for. Windows is untouched. ClientPathsTests looked for "odoSSH" in the profile directory. ClientPaths spells it DodoSSH on Windows and dodossh on Unix deliberately, one per platform convention, and that substring was clever enough to survive either spelling of the leading D while still only ever matching one of them. Now OrdinalIgnoreCase. WhyTheWindowItselfIsNeverShown asserted a COMException with HResult RPC_E_CHANGED_MODE, which is WebView2 refusing an MTA thread — a Win32 component raising a COM error. On Linux the adapter is a different implementation with no apartment to disagree about, so showing the window works and Should.Throw catches nothing. Skipped there rather than loosened to accept both outcomes: the assertion is the documentation in that test, and one that passed everywhere would have stopped recording the constraint it exists to record. The fourth was CI's alone, and the diagnosis is the useful part. All 69 layout tests failed on the runner while 6 failed here, which looked like missing fonts and was not: Avalonia's headless renderer is Skia, libSkiaSharp.so links against libfontconfig, and without it the suite dies in HeadlessUnitTestSession with a TypeInitializationException on SKImageInfo naming none of its actual subjects. The job installs the one library now. Verified in a container where fc-list returns zero and the suite passes regardless, because the application carries Inter itself — fonts were never the problem, only the thing that would have looked for them. The whole solution now passes on Linux: 19 suites, 1295 tests, 0 failures, 4 skipped, the end-to-end Testcontainers suite included. README and platform-flags.md said testing was Windows-only, which CI now contradicts on every push, so both say what is true instead and the two findings are written down where the next person will look for them. macOS is still untested and now says so on its own rather than hiding inside "not Windows". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
207 lines
7.5 KiB
C#
207 lines
7.5 KiB
C#
using System.Runtime.InteropServices;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
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.
|
|
//
|
|
// All of which is a fact about Windows. WebView2 is a Win32 component and RPC_E_CHANGED_MODE is a COM
|
|
// error code; on Linux the terminal's adapter is a different implementation with no apartment to
|
|
// disagree about, so showing the window simply works and Should.Throw has nothing to catch. Skipped
|
|
// rather than rewritten to accept either outcome, because the assertion is the documentation here —
|
|
// a version that passed on both platforms would have stopped recording the constraint it exists to
|
|
// record. What the rest of the suite relies on, that the harness measures extracted controls, holds
|
|
// on every platform regardless.
|
|
if (!OperatingSystem.IsWindows())
|
|
{
|
|
Assert.Skip("WebView2's apartment requirement, and the COM error it raises, are Windows-only.");
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|