AppAutomation is a framework for UI automation of Avalonia desktop applications. The target consumer flow is:
- You integrate the framework via NuGet, not by downloading source code;
- You create the canonical test topology with a single command;
- You write page objects and shared scenarios once;
- You run the same scenarios in both
HeadlessandFlaUI.
The resulting structure of a consumer repository should look like this:
tests/
MyApp.UiTests.Authoring/
MyApp.UiTests.Headless/
MyApp.UiTests.FlaUI/
MyApp.AppAutomation.TestHost/
Authoring owns page objects and shared tests. Headless and FlaUI only run these scenarios through different runtime adapters.
Supported baseline:
| Component | Support |
|---|---|
AppAutomation.Abstractions |
net8.0+ |
AppAutomation.Session.Contracts |
net8.0+ |
AppAutomation.TUnit |
net8.0+ |
AppAutomation.TestHost.Avalonia |
net8.0+ |
AppAutomation.Avalonia.Headless |
net8.0, net10.0 |
AppAutomation.Recorder.Avalonia |
net8.0, net10.0 |
AppAutomation.FlaUI |
net8.0-windows7.0, net10.0-windows7.0 |
FlaUI runtime |
Windows only |
| Template package | dotnet new |
| CLI tool | .NET tool, command appautomation |
Full matrix: docs/appautomation/compatibility.md
The commands below use the latest version available from your configured feed. The generated AppAutomation package references also float to the latest available version by default.
dotnet new install AppAutomation.TemplatesRecommended local tool manifest in the consumer repo:
dotnet new tool-manifest
dotnet tool install AppAutomation.ToolingFallback global install:
dotnet tool install --global AppAutomation.ToolingFrom the root of your consumer repository:
dotnet new appauto-avalonia --name MyAppExplicit floating package-version override:
dotnet new appauto-avalonia --name MyApp --AppAutomationVersion "*"The template will create:
tests/MyApp.UiTests.Authoringtests/MyApp.UiTests.Headlesstests/MyApp.UiTests.FlaUItests/MyApp.AppAutomation.TestHostAPPAUTOMATION_NEXT_STEPS.md
If the tool is installed via local manifest:
dotnet tool run appautomation doctor --repo-root .If the tool is installed globally:
appautomation doctor --repo-root .doctor checks:
- whether canonical topology exists;
- whether you've switched to source dependency instead of
PackageReference; - whether generated scaffold still contains placeholder values;
- whether
TargetFrameworkis compatible; - whether
NuGet.Configexists anywhere under the repository root; - whether SDK is pinned via
global.json.
The template creates the correct topology, but cannot know your AUT-specific bootstrap. Next, you need to do exactly the following things.
File:
tests/MyApp.AppAutomation.TestHost/MyAppAppLaunchHost.cs
You need to replace placeholder values:
- solution file name;
- relative path to desktop
.csproj; TargetFrameworkof AUT;- desktop executable name;
AvaloniaAppTypeused by generated headless hooks;CreateHeadlessLaunchOptions()with realWindowcreation.
Framework helpers that are already available out of the box:
AppAutomation.TestHost.Avalonia.AvaloniaDesktopLaunchHostAppAutomation.TestHost.Avalonia.AvaloniaHeadlessLaunchHostAppAutomation.TestHost.Avalonia.TemporaryDirectory
Minimum for the first iteration:
- root window;
- main tabs / navigation anchors;
- critical input/button/result controls;
- key child controls for composite widgets.
- explicit
AutomationProperties.Namefor controls you assert viaWaitUntilName*.
Example:
<TabControl automation:AutomationProperties.AutomationId="MainTabs">
<TabItem automation:AutomationProperties.AutomationId="SmokeTabItem" />
</TabControl>File:
tests/MyApp.UiTests.Headless/Infrastructure/HeadlessSessionHooks.cs
The generated hooks already call HeadlessRuntime.SetSession(...) through MyAppAppLaunchHost.AvaloniaAppType. Replace the placeholder app type in TestHost, then keep the generated hooks as-is unless your AUT needs custom session lifetime handling.
In the Authoring project you:
- declare
[UiControl(...)]for simple controls; - manually add composite abstractions if necessary;
- write shared scenarios once.
If you want to reduce the first manual authoring pass, attach AppAutomation.Recorder.Avalonia to your AUT and let it generate Authoring partials instead of runtime-specific tests.
- Keep page classes
partial. - Keep the shared scenario base class
partialtoo, because recorder output is emitted as an extra partial with[Test]methods. - Prefer stable
AutomationId;Namelocators are opt-in and intentionally treated as a weaker fallback. Savewrites into the canonicalAuthoringtarget, whileExport...writes the same generated pair into a folder you pick from the overlay.- Invalid or ambiguous steps can stay visible in overlay preview for debugging, but they are skipped on save and reported as
persisted/skipped. - The overlay keeps a step journal with
Remove,Ignore,Retry, andCopyactions, so you can clean up a recording session without restarting it. - Save and export are single-flight operations: while a save/export is running, the overlay shows a busy summary and blocks duplicate save/export clicks.
- The recorder UI is hosted in a separate opaque window, so it no longer follows or overlays the AUT window.
- Hotkeys, overlay behavior, selector validation, and custom assertion capture are configurable through
AppAutomationRecorderOptions.
Reference smoke path in this repository:
$env:APPAUTOMATION_RECORDER='1'
$env:APPAUTOMATION_RECORDER_SCENARIO='SmokeFlow'
dotnet run --project sample/DotnetDebug.Avalonia/DotnetDebug.Avalonia.csproj -c DebugThe sample writes generated files to sample/DotnetDebug.AppAutomation.Authoring/Recorded. The overlay can start or stop capture, save canonical partials, export the same output to another folder, keep a review-first step journal, and show either the latest AppAutomation DSL statement or the diagnostics that explain why a step is warning-only or invalid.
Custom assertion capture can be extended without forking the recorder:
var recorderOptions = new AppAutomationRecorderOptions();
recorderOptions.AssertionExtractors.Add(new MyStatusBadgeAssertionExtractor());
AppAutomationRecorder.Attach(mainWindow, recorderOptions);Commands:
dotnet test --project tests/MyApp.UiTests.Headless/MyApp.UiTests.Headless.csproj -c Debug
dotnet test --project tests/MyApp.UiTests.FlaUI/MyApp.UiTests.FlaUI.csproj -c DebugFor FlaUI on multi-monitor workstations, keep desktop placement as an opt-in TestHost parameter. The default CreateDesktopLaunchOptions() path keeps the old Windows/application placement behavior, while scenarios that need deterministic geometry can pass an explicit placement:
public static DesktopAppLaunchOptions CreateDesktopLaunchOptions(
string? buildConfiguration = null,
DesktopWindowPlacement? windowPlacement = null)
{
return AvaloniaDesktopLaunchHost.CreateLaunchOptions(
DesktopApp,
new AvaloniaDesktopLaunchOptions
{
BuildConfiguration = buildConfiguration ?? BuildConfigurationDefaults.ForAssembly(typeof(MyAppAppLaunchHost).Assembly),
WindowPlacement = windowPlacement
});
}Most FlaUI tests can keep the default launch:
DesktopAppSession.Launch(MyAppAppLaunchHost.CreateDesktopLaunchOptions());Tests that need a dedicated monitor and outer size can opt in:
DesktopAppSession.Launch(MyAppAppLaunchHost.CreateDesktopLaunchOptions(
windowPlacement: DesktopWindowPlacement.Centered(
monitor: DesktopMonitorSelector.FromIndex(1),
width: 1280,
height: 900)));Monitor indexes are zero-based after stable ordering: primary monitor first, then virtual desktop coordinates. DesktopMonitorSelector.LastAvailable selects the last monitor in that order, and resolves to the primary monitor on a single-monitor desktop. If Size is omitted, placement keeps the app's current outer window size. Placement uses the monitor working area by default, so taskbars and docked shell UI are avoided. WindowPlacement = null keeps the old launch behavior. FlaUI is still interactive desktop automation and may take focus; this option makes placement predictable and reduces interference, but does not make the run headless.
To run the whole generated solution from the repo root:
dotnet test --solution MyApp.sln -c DebugIf you see Headless session is not initialized. Call HeadlessRuntime.SetSession from test hooks., verify:
tests/MyApp.UiTests.Headless/Infrastructure/HeadlessSessionHooks.csis still active in your test runner hooks;- the hooks call
HeadlessRuntime.SetSession(...)before tests and clear it afterwards; MyAppAppLaunchHost.AvaloniaAppTypeis no longer a placeholder;- you're running the intended target with
dotnet test --project ...ordotnet test --solution MyApp.sln -c Debug.
AppAutomation now covers typical integration gaps that consumers used to write manually:
dotnet newtemplate for canonical Avalonia topology;appautomation doctor;- reusable
AppAutomation.TestHost.Avalonia; - desktop launch helpers with repo-root / project-path / build-before-launch;
- headless launch helpers on top of
BeforeLaunchAsync,CreateMainWindow,CreateMainWindowAsync; - adapter registration API via
WithAdapters(...); - built-in composite abstraction
ISearchPickerControlandWithSearchPicker(...); - package-based smoke path via
eng/smoke-consumer.ps1.
The framework cannot automate these honestly and completely, so these things remain on the consumer side:
- domain-specific test data and permissions;
- auth bypass / login story;
- exact startup semantics of AUT;
- decision on which secondary controls are better simplified with data;
- adding
AutomationIdin the AUT itself.
- Do not pull
src/AppAutomation.*into your consumer repo as source dependency unless there's an extreme reason. - Do not duplicate tests from
Authoringin runtime projects. - Do not start with a complex end-to-end path. Start with one critical smoke scenario.
- Do not automate all controls before login / startup / settings path is stabilized.
- Do not hide repo-specific bootstrap inside a reusable framework package.
Working reference in this repository:
- sample/DotnetDebug.AppAutomation.Authoring
- sample/DotnetDebug.AppAutomation.Avalonia.Headless.Tests
- sample/DotnetDebug.AppAutomation.FlaUI.Tests
- sample/DotnetDebug.AppAutomation.TestHost
- Step-by-step consumer flow: docs/appautomation/quickstart.md
- Pre-flight checklist: docs/appautomation/adoption-checklist.md
- Canonical project responsibilities: docs/appautomation/project-topology.md
- Selector contract for both runtimes: docs/appautomation/selector-contract.md
- Advanced bootstrap and composite controls: docs/appautomation/advanced-integration.md
- Packaging and release flow: docs/appautomation/publishing.md
AppAutomation — это фреймворк для автоматизации пользовательского интерфейса настольных приложений на Avalonia. Типовой сценарий использования выглядит так:
- вы подключаете фреймворк через NuGet, а не скачиваете исходный код;
- одной командой создаёте стандартную структуру тестов;
- один раз описываете объекты страниц и общие сценарии;
- запускаете те же сценарии и в
Headless, и вFlaUI.
Итоговая структура репозитория-потребителя должна выглядеть так:
tests/
MyApp.UiTests.Authoring/
MyApp.UiTests.Headless/
MyApp.UiTests.FlaUI/
MyApp.AppAutomation.TestHost/
Authoring содержит объекты страниц и общие тесты. Headless и FlaUI только запускают эти же сценарии через разные адаптеры выполнения.
Поддерживаемая базовая конфигурация:
| Компонент | Поддержка |
|---|---|
AppAutomation.Abstractions |
net8.0+ |
AppAutomation.Session.Contracts |
net8.0+ |
AppAutomation.TUnit |
net8.0+ |
AppAutomation.TestHost.Avalonia |
net8.0+ |
AppAutomation.Avalonia.Headless |
net8.0, net10.0 |
AppAutomation.Recorder.Avalonia |
net8.0, net10.0 |
AppAutomation.FlaUI |
net8.0-windows7.0, net10.0-windows7.0 |
Среда выполнения FlaUI |
только Windows |
| Пакет шаблонов | dotnet new |
| Инструмент командной строки | .NET tool, команда appautomation |
Полная матрица: docs/appautomation/compatibility.md
Команды ниже используют последнюю доступную версию из настроенного feed.
Сгенерированные PackageReference для AppAutomation по умолчанию тоже используют последнюю доступную версию.
dotnet new install AppAutomation.TemplatesРекомендуемый локальный tool manifest в репозитории-потребителе:
dotnet new tool-manifest
dotnet tool install AppAutomation.ToolingРезервный глобальный вариант:
dotnet tool install --global AppAutomation.ToolingИз корня вашего репозитория-потребителя:
dotnet new appauto-avalonia --name MyAppЯвный floating override для версии пакетов:
dotnet new appauto-avalonia --name MyApp --AppAutomationVersion "*"Шаблон создаст:
tests/MyApp.UiTests.Authoringtests/MyApp.UiTests.Headlesstests/MyApp.UiTests.FlaUItests/MyApp.AppAutomation.TestHostAPPAUTOMATION_NEXT_STEPS.md
Если инструмент установлен через локальный manifest:
dotnet tool run appautomation doctor --repo-root .Если инструмент установлен глобально:
appautomation doctor --repo-root .doctor проверяет:
- существует ли стандартная структура тестов;
- не перешли ли вы на зависимость в виде исходного кода вместо
PackageReference; - содержит ли сгенерированный scaffold ещё неубранные placeholder-значения;
- совместимы ли
TargetFramework; - есть ли
NuGet.Configгде-либо под корнем репозитория; - закреплён ли SDK через
global.json.
Шаблон создаёт правильную структуру, но не может знать особенности запуска вашего AUT. После генерации нужно сделать следующее.
Файл:
tests/MyApp.AppAutomation.TestHost/MyAppAppLaunchHost.cs
Нужно заменить значения-заглушки:
- имя файла решения;
- относительный путь к настольному
.csproj; TargetFrameworkдля AUT;- имя исполняемого файла настольного приложения;
AvaloniaAppType, который используют сгенерированные headless hooks;CreateHeadlessLaunchOptions()с реальным созданиемWindow.
Вспомогательные классы фреймворка, которые уже доступны:
AppAutomation.TestHost.Avalonia.AvaloniaDesktopLaunchHostAppAutomation.TestHost.Avalonia.AvaloniaHeadlessLaunchHostAppAutomation.TestHost.Avalonia.TemporaryDirectory
Минимум для первой итерации:
- корневое окно;
- основные вкладки и опорные элементы навигации;
- критичные поля ввода, кнопки и элементы с результатами;
- ключевые дочерние элементы в составных элементах интерфейса.
- явный
AutomationProperties.Nameдля тех элементов, которые будут участвовать вWaitUntilName*.
Пример:
<TabControl automation:AutomationProperties.AutomationId="MainTabs">
<TabItem automation:AutomationProperties.AutomationId="SmokeTabItem" />
</TabControl>Файл:
tests/MyApp.UiTests.Headless/Infrastructure/HeadlessSessionHooks.cs
Сгенерированные hooks уже вызывают HeadlessRuntime.SetSession(...) через MyAppAppLaunchHost.AvaloniaAppType. Обычно достаточно заменить placeholder-типа приложения в TestHost и оставить hooks без изменений, если AUT не требует особого жизненного цикла сеанса.
В проекте Authoring вы:
- объявляете
[UiControl(...)]для простых элементов управления; - при необходимости вручную добавляете составные абстракции;
- один раз пишете общие сценарии.
Если не хочется вручную проходить весь первый цикл authoring-кода, можно подключить AppAutomation.Recorder.Avalonia к AUT и генерировать partial-файлы прямо в Authoring, а не отдельные runtime-specific тесты.
- Классы страниц должны оставаться
partial. - Общий scenario base class тоже должен быть
partial, потому что recorder добавляет новые[Test]-методы в отдельный partial. - Основной контракт селекторов для recorder-а это
AutomationId;Nameвключается только осознанно и считается более слабым fallback. Saveпишет в каноническую директориюAuthoring, аExport...сохраняет ту же пару generated partials в выбранную папку.- Невалидные или неоднозначные шаги можно оставить в preview для отладки, но при сохранении они пропускаются и попадают в статус как
persisted/skipped. - Overlay держит step journal с действиями
Remove,Ignore,RetryиCopy, так что плохой шаг можно выкинуть или отложить без полного перезапуска записи. SaveиExport...теперь single-flight: пока идёт запись файлов, overlay показывает busy summary и не даёт запустить второй save/export поверх первого.- Recorder UI теперь живёт в отдельном непрозрачном окне и больше не привязан к позиции или состоянию окна AUT.
- Hotkeys, поведение overlay, selector validation и кастомный assertion capture настраиваются через
AppAutomationRecorderOptions.
Референсный smoke path в этом репозитории:
$env:APPAUTOMATION_RECORDER='1'
$env:APPAUTOMATION_RECORDER_SCENARIO='SmokeFlow'
dotnet run --project sample/DotnetDebug.Avalonia/DotnetDebug.Avalonia.csproj -c DebugSample сохраняет generated partials в sample/DotnetDebug.AppAutomation.Authoring/Recorded. Overlay позволяет запускать и останавливать запись, сохранять канонические partials, экспортировать тот же output в другую директорию, просматривать и править session-level step journal и сразу видеть либо последний AppAutomation DSL-вызов, либо диагностику, почему конкретный шаг остался warning-only или invalid.
Кастомный assertion capture можно подключить без форка recorder-а:
var recorderOptions = new AppAutomationRecorderOptions();
recorderOptions.AssertionExtractors.Add(new MyStatusBadgeAssertionExtractor());
AppAutomationRecorder.Attach(mainWindow, recorderOptions);Команды:
dotnet test --project tests/MyApp.UiTests.Headless/MyApp.UiTests.Headless.csproj -c Debug
dotnet test --project tests/MyApp.UiTests.FlaUI/MyApp.UiTests.FlaUI.csproj -c DebugДля FlaUI на рабочих станциях с несколькими мониторами держите положение окна как opt-in параметр TestHost. Обычный путь CreateDesktopLaunchOptions() сохраняет прежнее поведение Windows/приложения, а сценарии с требованием к геометрии могут передать явный placement:
public static DesktopAppLaunchOptions CreateDesktopLaunchOptions(
string? buildConfiguration = null,
DesktopWindowPlacement? windowPlacement = null)
{
return AvaloniaDesktopLaunchHost.CreateLaunchOptions(
DesktopApp,
new AvaloniaDesktopLaunchOptions
{
BuildConfiguration = buildConfiguration ?? BuildConfigurationDefaults.ForAssembly(typeof(MyAppAppLaunchHost).Assembly),
WindowPlacement = windowPlacement
});
}Большинство FlaUI-тестов могут оставаться на обычном запуске:
DesktopAppSession.Launch(MyAppAppLaunchHost.CreateDesktopLaunchOptions());Тесты, которым нужен отдельный монитор и внешний размер окна, включают placement явно:
DesktopAppSession.Launch(MyAppAppLaunchHost.CreateDesktopLaunchOptions(
windowPlacement: DesktopWindowPlacement.Centered(
monitor: DesktopMonitorSelector.FromIndex(1),
width: 1280,
height: 900)));Индексы мониторов начинаются с нуля после стабильной сортировки: сначала основной монитор, затем координаты виртуального рабочего стола. DesktopMonitorSelector.LastAvailable выбирает последний монитор в этом порядке, а на одномониторной машине резолвится в основной монитор. Если Size не задан, placement сохраняет текущий внешний размер окна приложения. По умолчанию используется рабочая область монитора, поэтому taskbar и docked shell UI не перекрываются. WindowPlacement = null сохраняет прежнее поведение запуска. FlaUI остаётся интерактивной desktop-автоматизацией и может забирать фокус; эта настройка делает размещение предсказуемым и снижает помехи, но не превращает запуск в headless.
Чтобы запустить всё сгенерированное решение из корня репозитория:
dotnet test --solution MyApp.sln -c DebugЕсли вы видите Headless session is not initialized. Call HeadlessRuntime.SetSession from test hooks., проверьте:
- что
tests/MyApp.UiTests.Headless/Infrastructure/HeadlessSessionHooks.csпо-прежнему подключён в hooks тестового раннера; - что hooks вызывают
HeadlessRuntime.SetSession(...)до тестов и очищают его после завершения; - что
MyAppAppLaunchHost.AvaloniaAppTypeуже не содержит placeholder; - что вы запускаете нужную цель через
dotnet test --project ...илиdotnet test --solution MyApp.sln -c Debug.
AppAutomation уже закрывает типичные проблемы интеграции, которые раньше приходилось решать вручную:
- шаблон
dotnet newдля стандартной структуры проектов Avalonia; appautomation doctor;- переиспользуемый
AppAutomation.TestHost.Avalonia; - вспомогательные средства запуска настольного приложения с
repo-root,project-pathиbuild-before-launch; - вспомогательные средства запуска
HeadlessповерхBeforeLaunchAsync,CreateMainWindow,CreateMainWindowAsync; - API регистрации адаптеров через
WithAdapters(...); - встроенная составная абстракция
ISearchPickerControlиWithSearchPicker(...); - готовый сценарий быстрой проверки через
eng/smoke-consumer.ps1.
Фреймворк не может полностью и надёжно автоматизировать следующие вещи, поэтому они остаются на стороне потребителя:
- предметно-ориентированные тестовые данные и права доступа;
- обход аутентификации и сценарий входа;
- точное поведение AUT при запуске;
- решение о том, какие второстепенные элементы лучше упростить данными;
- добавление
AutomationIdв самом AUT.
- Не подтягивайте
src/AppAutomation.*в репозиторий-потребитель как зависимость в виде исходного кода, если для этого нет совсем крайней причины. - Не дублируйте тесты из
Authoringв проектахHeadlessиFlaUI. - Не начинайте со сложного сквозного сценария. Сначала нужен один критичный сценарий быстрой проверки.
- Не автоматизируйте все элементы подряд, пока не стабилизированы вход, запуск и путь через настройки.
- Не прячьте специфичную для репозитория логику запуска внутрь переиспользуемого пакета фреймворка.
В этом репозитории есть рабочий пример:
- sample/DotnetDebug.AppAutomation.Authoring
- sample/DotnetDebug.AppAutomation.Avalonia.Headless.Tests
- sample/DotnetDebug.AppAutomation.FlaUI.Tests
- sample/DotnetDebug.AppAutomation.TestHost
- Пошаговый сценарий подключения: docs/appautomation/quickstart.md
- Проверочный список перед стартом: docs/appautomation/adoption-checklist.md
- Роли проектов в стандартной структуре: docs/appautomation/project-topology.md
- Контракт селекторов для обоих рантаймов: docs/appautomation/selector-contract.md
- Расширенная инициализация и составные элементы управления: docs/appautomation/advanced-integration.md
- Упаковка и процесс выпуска: docs/appautomation/publishing.md