agentleFS
linuxdevellinuxdevel/Avalonia-skills/skills/avalonia/avalonia-testing/SKILL.md

avalonia-testing

Skill50 starsChanged 7 days agoMITView on GitHub

Use when writing tests for Avalonia apps — headless UI testing, ViewModel unit tests, UI automation, or setting up the Avalonia test infrastructure.

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    No special setup needed — ViewModels are plain C# classes:

    line 11
  2. Very specific:

    Test framework: 12.0+ requires xUnit v3 / NUnit 4 (Avalonia.Headless.XUnit 12.1.4 depends on xunit.v3.extensibility.core).

    line 42

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Avalonia Testing

Overview

Two testing approaches: ViewModel unit tests (no Avalonia needed, plain xUnit/NUnit), and headless UI tests using Avalonia.Headless (renders without a display).

ViewModel Unit Tests (No UI)

No special setup needed — ViewModels are plain C# classes:

[Fact]
public void Name_Change_RaisesPropertyChanged()
{
    var vm = new MainViewModel();
    var raised = new List<string?>();
    vm.PropertyChanged += (_, e) => raised.Add(e.PropertyName);

    vm.Name = "Alice";

    Assert.Contains(nameof(vm.Name), raised);
    Assert.Equal("Alice", vm.Name);
}

[Fact]
public async Task SaveCommand_WhenNameEmpty_CannotExecute()
{
    var vm = new MainViewModel { Name = "" };
    Assert.False(vm.SaveCommand.CanExecute(null));
}

Headless Testing Setup

NuGet packages (same version as Avalonia):

  • Avalonia.Headless
  • Avalonia.Headless.XUnit (or Avalonia.Headless.NUnit)
  • Avalonia.Themes.Fluent (controls need a theme)
  • Avalonia.Skia (real rendering for screenshots)
  • Avalonia.HarfBuzz (12.0+; omit on 11.x)

Test framework: 12.0+ requires xUnit v3 / NUnit 4 (Avalonia.Headless.XUnit 12.1.4 depends on xunit.v3.extensibility.core). (11.x) xUnit v2 / NUnit 3.

// Assembly-level attribute (AssemblyInfo.cs or top of test file)
[assembly: AvaloniaTestApplication(typeof(TestAppBuilder))]

public class TestAppBuilder
{
    public static AppBuilder BuildAvaloniaApp()
        => AppBuilder.Configure<App>()
            .UseSkia()
            .UseHarfBuzz() // 12.0+; omit on 11.x
            .UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false });
}

CaptureRenderedFrame() requires Skia with UseHeadlessDrawing = false. The lightweight UseHeadlessDrawing = true backend is suitable for tests that do not capture screenshots. Avalonia 12 also requires UseHarfBuzz() when configuring Skia explicitly. On Avalonia 11.x, omit both the Avalonia.HarfBuzz package and the .UseHarfBuzz() call.

Optional isolation: [assembly: AvaloniaTestIsolation(AvaloniaTestIsolationLevel.PerAssembly)] (default PerTest recreates Application + Dispatcher; PerAssembly shares state, no concurrent tests). Use [AvaloniaTheory] instead of [Theory].

Writing Headless Tests (xUnit)

public class MainWindowTests
{
    [AvaloniaFact]
    public void Button_Click_UpdatesViewModel()
    {
        var window = new MainWindow();
        var vm = new MainViewModel();
        window.DataContext = vm;
        window.Show();

        var button = window.FindControl<Button>("SaveButton")!;
        button.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));

        Assert.True(vm.SaveWasCalled);
    }

    [AvaloniaFact]
    public async Task TextBox_Input_UpdatesBinding()
    {
        var window = new MainWindow();
        window.DataContext = new MainViewModel();
        window.Show();

        var textBox = window.FindControl<TextBox>("NameBox")!;
        textBox.RaiseTextInput("Hello");

        await Task.Delay(50); // allow binding to propagate
        Assert.Equal("Hello", ((MainViewModel)window.DataContext!).Name);
    }
}

Finding Controls

// By name (x:Name)
var btn = window.FindControl<Button>("MyButton");

// By type (first match)
var textBox = window.FindDescendantOfType<TextBox>();

// All of type
var allButtons = window.GetVisualDescendants().OfType<Button>();

Simulating Input

// Keyboard
window.KeyPressQwerty(PhysicalKey.Enter, RawInputModifiers.None);
window.KeyTextInput("Hello");

// Mouse/Pointer
window.MouseDown(new Point(100, 50), MouseButton.Left);
window.MouseMove(new Point(150, 50));
window.MouseUp(new Point(150, 50), MouseButton.Left);

// DPI / render scaling (12.0+)
window.SetRenderScaling(2.0);

Screenshot Testing

[AvaloniaFact]
public void Window_RendersCorrectly()
{
    var window = new MainWindow();
    window.Show();

    // Capture frame as bitmap
    var bitmap = window.CaptureRenderedFrame()!;
    // Compare or save for regression
    bitmap.Save("snapshot.png");
}

Testing with NUnit

[TestFixture]
public class Tests
{
    [AvaloniaTest]  // instead of [Test]
    public void MyTest() { }
}

Community Testing Libraries

LibraryNuGetPurpose
Verify.AvaloniaVerify.AvaloniaExtends Verify for snapshot/approval testing of Avalonia UI

Snapshot Testing with Verify.Avalonia

[AvaloniaFact]
public async Task Window_MatchesSnapshot()
{
    var window = new MainWindow();
    window.Show();

    await Verify(window); // saves .verified.png on first run, diffs on subsequent
}

Requires: Verify.Avalonia + Verify.Xunit (or NUnit variant). On first run, creates a .verified.png baseline. Subsequent runs diff against it — fails on visual regression.

Common Mistakes

  • 12.x project still on xUnit v2 / NUnit 3 — Headless packages need xUnit v3 / NUnit 4
  • Missing [assembly: AvaloniaTestApplication] — headless renderer not initialized, NullReferenceException
  • Using [Fact] instead of [AvaloniaFact] for UI tests — no Avalonia dispatcher, random failures
  • Forgetting window.Show() before interacting — controls not attached to visual tree
  • Testing bindings synchronously — bindings may be async, add small delay or use Dispatcher.UIThread.RunJobs()

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.