#398 was a crash on every modal open/close caused by MainWindow's constructor calling AvaloniaXamlLoader.Load(this) instead of the generated InitializeComponent() — only InitializeComponent assigns the x:Name backing fields, so every named control was null and the first Dispatcher.UIThread.Post callback in OnViewModelPropertyChanged threw NullReferenceException, killing the process. It reached the user gate because no test in tests/AcDream.Launcher.Tests (ViewModel-only) ever constructed a MainWindow. #399 is the process gap that let that class of defect through 14,012 green tests. Adds Avalonia.Headless.XUnit 12.1.1 to the launcher test project. Its net10.0 dependency group targets xunit v3, so the project migrates xunit 2.9.3 -> xunit.v3 3.2.2 (drop-in: all 54 pre-existing tests compile and pass unchanged under dotnet test via xunit.runner.visualstudio 3.1.4, which already supported v1/v2/v3; two call sites needed TestContext.Current.CancellationToken per the new xUnit1051 analyzer). TestAppBuilder.cs wires [assembly: AvaloniaTestApplication] to a headless AppBuilder.Configure<App>() so the real App.axaml FluentTheme is live in tests. MainWindowViewTests.cs adds 12 [AvaloniaFact]/[AvaloniaTheory] tests: - an explicit non-null + type check of every x:Name field the code-behind dereferences (ProfilesTree, ServerNameTextBox, AccountNameTextBox, CharacterNameTextBox, EditorSubmitButton, FirstRunDatDirectoryTextBox, FirstRunCloseButton, UpdateCloseButton) - a reflection sweep over every x:Name found in MainWindow.axaml, so a future named control without a matching non-null field fails loudly - one open+close round trip per ProfileEditorKind (all seven, including Remove), plus the first-run wizard and the update prompt, each pumping Dispatcher.UIThread.RunJobs() so the queued focus callback actually executes instead of just being asserted vacuously - a dedicated test for the _focusBeforeModal-restore branch (not just the ProfilesTree.Focus() fallback), anchored on a real focusable button since ProfilesTree (TreeView) has Focusable="False" under FluentTheme — its own tab stops are TreeViewItem rows, so the close-path assertions check "no exception escaped the dispatcher" rather than "focus landed on ProfilesTree" Falsification (required evidence): reverting MainWindow's constructor to AvaloniaXamlLoader.Load(this) and rerunning gives 12 failed / 0 passed — 10 tests throw NullReferenceException at MainWindow.FocusActiveModal, propagating cleanly out of Dispatcher.UIThread.RunJobs() (confirming dispatcher exceptions are not silently swallowed), and the 2 reflection tests fail on an explicit "x:Name 'ProfilesTree' was null after construction" message. Restoring InitializeComponent() gives 12 passed / 0 failed. Full launcher suite: 66 passed / 0 failed, reproduced on both Windows and native Ubuntu (WSL, no display/Xvfb — Avalonia.Headless needs none). AcDream.Launcher.Core.Tests: 317/317 unaffected. No CI workflow change needed: .github/workflows/headless-portability.yml's portable-launcher job already runs dotnet test on the launcher test project on both windows-latest and ubuntu-latest with no display setup, which is sufficient for Avalonia.Headless. Closes #399. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
21 lines
799 B
C#
21 lines
799 B
C#
using Avalonia;
|
|
using Avalonia.Headless;
|
|
|
|
[assembly: AvaloniaTestApplication(typeof(AcDream.Launcher.Tests.TestAppBuilder))]
|
|
|
|
namespace AcDream.Launcher.Tests;
|
|
|
|
/// <summary>
|
|
/// Headless Avalonia application entry point for <c>[AvaloniaFact]</c>/
|
|
/// <c>[AvaloniaTheory]</c> tests (see #399). Reuses the launcher's real
|
|
/// <see cref="App"/> so the FluentTheme styles declared in App.axaml are
|
|
/// live for every test — <see cref="MainWindow"/>'s controls (TreeView,
|
|
/// Button, TextBox, ...) need a real control theme to template and focus
|
|
/// correctly, not just an unstyled visual tree.
|
|
/// </summary>
|
|
public static class TestAppBuilder
|
|
{
|
|
public static AppBuilder BuildAvaloniaApp() =>
|
|
AppBuilder.Configure<App>()
|
|
.UseHeadless(new AvaloniaHeadlessPlatformOptions());
|
|
}
|