Goal
Avalonia.Headless (AvaloniaHeadlessPlatformExtensions.UseHeadless).Why this matters
Prerequisites
Add packages to your test project:
Avalonia.HeadlessAvalonia.Headless.XUnit or Avalonia.Headless.NUnitAvalonia.Skia (only if you need rendered frames)AssemblyInfo.cs)using Avalonia;
using Avalonia.Headless;
using Avalonia.Headless.XUnit;
[assembly: AvaloniaTestApplication(typeof(TestApp))]
public sealed class TestApp : Application
{
public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<TestApp>()
.UseHeadless(new AvaloniaHeadlessPlatformOptions
{
UseHeadlessDrawing = true, // set false + UseSkia for frame capture
UseCpuDisabledRenderLoop = true
})
.AfterSetup(_ => Dispatcher.UIThread.VerifyAccess());
}
UseHeadlessDrawing = true skips Skia (fast). For pixel tests, set false and call .UseSkia().
Use [AvaloniaTestApp] attribute (from Avalonia.Headless.NUnit) and the provided AvaloniaTestFixture base.
public class TextBoxTests
{
[AvaloniaFact]
public async Task TextBox_Receives_Typed_Text()
{
var textBox = new TextBox { Width = 200, Height = 24 };
var window = new Window { Content = textBox };
window.Show();
// Focus on UI thread
await Dispatcher.UIThread.InvokeAsync(() => textBox.Focus());
window.KeyTextInput("Avalonia");
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
Assert.Equal("Avalonia", textBox.Text);
}
}
Helpers from Avalonia.Headless add extension methods to TopLevel/Window (KeyTextInput, KeyPress, MouseDown, etc.). Always call ForceRenderTimerTick() after inputs to flush layout/bindings.
[ AvaloniaFact ]
public async Task Button_Click_Executes_Command()
{
var commandExecuted = false;
var button = new Button
{
Width = 100,
Height = 30,
Content = "Click me",
Command = ReactiveCommand.Create(() => commandExecuted = true)
};
var window = new Window { Content = button };
window.Show();
await Dispatcher.UIThread.InvokeAsync(() => button.Focus());
window.MouseDown(button.Bounds.Center, MouseButton.Left);
window.MouseUp(button.Bounds.Center, MouseButton.Left);
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
Assert.True(commandExecuted);
}
Bounds.Center obtains center point from Control.Bounds. For container-based coordinates, offset appropriately.
Configure Skia rendering in test app builder:
public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<TestApp>()
.UseSkia()
.UseHeadless(new AvaloniaHeadlessPlatformOptions
{
UseHeadlessDrawing = false,
UseCpuDisabledRenderLoop = true
});
Capture frames:
[ AvaloniaFact ]
public void Border_Renders_Correct_Size()
{
var border = new Border
{
Width = 200,
Height = 100,
Background = Brushes.Red
};
var window = new Window { Content = border };
window.Show();
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
using var frame = window.GetLastRenderedFrame();
Assert.Equal(200, frame.Size.Width);
Assert.Equal(100, frame.Size.Height);
// Optional: save to disk for debugging
// frame.Save("border.png");
}
Compare pixels to baseline image using e.g., ImageMagick or custom diff with tolerance. Keep baselines per theme/resolution to avoid false positives.
If you need Avalonia to drive the render loop before reading pixels, call CaptureRenderedFrame() instead of GetLastRenderedFrame()—it schedules a composition pass and forces a render tick. This mirrors what desktop renderers do when they flush the CompositionTarget, keeping the snapshot pipeline close to production.
HeadlessUnitTestSession.StartNew(typeof(App)) when you need deterministic startup logic outside the provided xUnit/NUnit attributes. Wrap it in IAsyncLifetime so tests share a dispatcher loop safely.AvaloniaLocator scope and injecting fakes (e.g., mock IClipboard, stub IStorageProvider).ShowControlAsync<TControl>()) that create a Window, attach the control, call ForceRenderTimerTick, and return the control for assertions.AutomationPeer.CreatePeerForElement(control) and assert patterns (InvokePattern, ValuePattern) without relying on visual tree traversal.external/Avalonia/tests/Avalonia.Headless.UnitTests for patterns that wrap AppBuilder and expose helpers for reuse across cases.For debugging, you can run headless with a VNC server and observe the UI.
AppBuilder.Configure<App>()
.UseHeadless(new AvaloniaHeadlessPlatformOptions { UseVnc = true, UseSkia = true })
.StartWithClassicDesktopLifetime(args);
Connect with a VNC client to view frames and interact.
Use AvaloniaHeadlessPlatform.ForceRenderTimerTick() to advance timers. For DispatcherTimer or animations, call it repeatedly.
For file-based assertions, use in-memory streams or temp directories. Avoid writing to the repo path; tests should be self-cleaning.
Dispatcher.UIThread.InvokeAsync for UI updates..Result or .Wait().async Task WaitForAsync(Func<bool> condition, TimeSpan timeout)
{
var deadline = DateTime.UtcNow + timeout;
while (!condition())
{
if (DateTime.UtcNow > deadline)
throw new TimeoutException("Condition not met");
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
await Task.Delay(10);
}
}
dotnet test in GitHub Actions/Azure Pipelines/GitLab.Xvfb).DragDrop helpers and confirm target list received data.tests/BaselineImages.AutomationPeer patterns (Invoke/Value) to validate accessibility contracts alongside visual assertions.AvaloniaHeadlessPlatform.csHeadlessUnitTestSession.csHeadlessWindowExtensionsAvalonia.Headless.XUnit, Avalonia.Headless.NUnittests/Avalonia.Headless.UnitTests, tests/Avalonia.Headless.XUnit.UnitTestsWhat's next