flaui-tests
Authors and runs FlaUI-based Windows UI tests - the .NET-native wrapper around Microsoft UI Automation (UIA2 + UIA3). Covers the `FlaUI.Core` / `FlaUI.UIA2` / `FlaUI.UIA3` NuGet packages, `Application.Launch` / `Application.Attach` lifecycles, `ConditionFactory` + `FindFirstDescendant` locator patterns, `Retry` waits, and xUnit / NUnit / MSTest harness integration. Use when the test stack is C# / .NET-first and the team wants idiomatic in-process UIA calls rather than the HTTP/JSON wire protocol of `winappdriver` (direct or Appium-wrapped).
Install with skills.sh (any agent)
npx skills add testland/qa --skill flaui-testsflaui-tests
Overview
FlaUI is a .NET library for automated UI testing of Windows applications (flaui (opens in new window)) that wraps Microsoft UI Automation (UIA) - the Windows accessibility tree covered in desktop-test-strategy-reference - behind an idiomatic C# API. It supports "Win32, WinForms, WPF, and Store Apps" via two bindings: UIA2 (managed System.Windows.Automation, Microsoft Learn - UI Automation Overview (opens in new window)) and UIA3 (COM interop). v5.0.0 released February 2025; MIT-licensed and actively maintained (flaui (opens in new window)).
Disambiguation - FlaUI vs winappdriver
FlaUI is a .NET library that links into the test process and calls UIA directly. By contrast, winappdriver is an HTTP/JSON service that exposes a W3C-WebDriver endpoint on 127.0.0.1:4723; tests speak Selenium-style protocol over the wire, the driver is language-agnostic, and it can be invoked either directly or through the Appium 2.x wrapper (gestures / multi-window helpers / PowerShell hooks - both paths in that skill).
Pick FlaUI when the test stack is already C# / .NET-first and you want in-process UIA calls without an HTTP hop. Pick winappdriver when you need a Selenium client in another language or the Appium feature surface.
When to use
For cross-language test stacks (Java / Python / Ruby clients), use winappdriver instead.
Authoring
NuGet packages
Per flaui (opens in new window), three packages cover the surface:
| Package | Purpose |
|---|---|
FlaUI.Core | Base library - element abstractions, Application, ConditionFactory, Retry, control patterns |
FlaUI.UIA3 | COM-based UIA binding - recommended for WPF and Store Apps (flaui (opens in new window)) |
FlaUI.UIA2 | Managed UIA binding using System.Windows.Automation (msuia2 (opens in new window)) - better legacy WinForms compatibility (flaui (opens in new window)) |
Reference both FlaUI.Core and one of UIA2 / UIA3 from the test project. Mixed-mode authoring (UIA2 and UIA3 in the same process) is unsupported - see FlaUInspect (opens in new window) which requires the inspector mode to be picked at startup.
Launching the application under test
Per the FlaUI wiki - Application page (opens in new window):
using FlaUI.Core;
using FlaUI.UIA3;
// Launch a fresh process
var app = Application.Launch(@"C:\Path\To\MyApp.exe");
// Attach to an already-running process by name or PID
var existing = Application.Attach("MyApp");
// Best-effort: attach if running, launch otherwise
var aol = Application.AttachOrLaunch(new ProcessStartInfo(@"C:\Path\To\MyApp.exe"));
// For a Windows Store app, pass the AUMID
var store = Application.LaunchStoreApp("Microsoft.WindowsCalculator_8wekyb3d8bbwe!App");
using var automation = new UIA3Automation();
var window = app.GetMainWindow(automation);Per flauiapp (opens in new window): "When the application object is disposed, the application itself is closed as well." Pair the Application lifecycle with the test harness's fixture scope so child processes are cleaned up after each test class.
Finding elements with ConditionFactory
The lambda form is the shortest and is the upstream convention (flauisearch (opens in new window)):
var loginButton = window.FindFirstDescendant(cf => cf.ByAutomationId("LoginButton"));Prefer ByAutomationId (developer-set, locale- and theme-independent per msuia2 (opens in new window)); fall back to ByControlType + a nested condition, then ByName as a last resort. The equivalent ConditionFactory / PropertyCondition forms, the FindFirst* / FindAll* families, and the full condition-constructor list are in references/flaui-api.md.
Interacting with elements
Per flaui (opens in new window):
// Strongly-typed wrappers
var button = window.FindFirstDescendant(cf => cf.ByAutomationId("Submit")).AsButton();
button.Invoke();
var textbox = window.FindFirstDescendant(cf => cf.ByAutomationId("Username")).AsTextBox();
textbox.Enter("alice@example.com");
var listbox = window.FindFirstDescendant(cf => cf.ByControlType(ControlType.List)).AsListBox();
listbox.Select(2);AsButton().Invoke() calls the UIA InvokePattern on the element - the accessibility-canonical "press" action, distinct from a synthetic mouse click (msuia2 (opens in new window) §Control Patterns).
Waits with the Retry class
Before v2.0.0 some Find methods auto-retried; that responsibility now falls to the caller (flauiretry (opens in new window)):
// Wait until the element appears
var found = Retry.WhileNull(
() => window.FindFirstDescendant(cf => cf.ByAutomationId("StatusLabel")),
timeout: TimeSpan.FromSeconds(10),
interval: TimeSpan.FromMilliseconds(200),
throwOnTimeout: true,
ignoreException: true).Result;
// Wait until the element disappears
Retry.WhileTrue(
() => window.FindFirstDescendant(cf => cf.ByAutomationId("Spinner")) is not null,
timeout: TimeSpan.FromSeconds(30));Retry.WhileNull / Retry.WhileTrue / Retry.WhileFalse / Retry.WhileException are the four variants (flauiretry (opens in new window)). Each returns a RetryResult carrying iteration count, duration, and the last value - the test can assert on those metrics when diagnosing slow-loading screens.
Waits with Application.WaitWhileBusy
WaitWhileBusy blocks while the target process is busy; a null timeout means infinite, and it returns true if the application went idle (flauiappsrc (opens in new window)):
public bool WaitWhileBusy(TimeSpan? waitTimeout = null)Use it after a launch or a window-level action (menu open, modal dismiss, dialog confirm) before driving the next element - it blocks on the Win32 message-pump-idle signal of the target process. Pair with WaitWhileMainHandleIsMissing right after Launch so the test doesn't race the splash screen:
var app = Application.Launch(@"C:\Path\To\InvoiceApp.exe");
app.WaitWhileMainHandleIsMissing(TimeSpan.FromSeconds(10));
app.WaitWhileBusy(TimeSpan.FromSeconds(10));
var window = app.GetMainWindow(automation);
window.FindFirstDescendant(cf => cf.ByAutomationId("Save")).AsButton().Invoke();
app.WaitWhileBusy(TimeSpan.FromSeconds(5)); // wait for save handlerRetry.* waits on element-level conditions (descendant appears / disappears / matches a predicate); WaitWhileBusy waits on the process-level idle signal. Both belong in the same test - pick by what you can actually observe.
Running
Test framework integration
FlaUI integrates with any .NET test runner - xUnit, NUnit, MSTest:
// xUnit collection fixture for one-time app launch per test class
public class LoginAppFixture : IDisposable
{
public Application App { get; }
public UIA3Automation Automation { get; }
public LoginAppFixture()
{
App = Application.Launch(@"C:\Path\To\LoginApp.exe");
Automation = new UIA3Automation();
}
public void Dispose()
{
Automation.Dispose();
App.Close();
App.Dispose();
}
}
public class LoginTests : IClassFixture<LoginAppFixture>
{
private readonly LoginAppFixture _fx;
public LoginTests(LoginAppFixture fx) => _fx = fx;
[Fact]
public void Logs_in_with_valid_credentials()
{
var window = _fx.App.GetMainWindow(_fx.Automation);
window.FindFirstDescendant(cf => cf.ByAutomationId("User")).AsTextBox().Enter("alice");
window.FindFirstDescendant(cf => cf.ByAutomationId("Pass")).AsTextBox().Enter("secret");
window.FindFirstDescendant(cf => cf.ByAutomationId("Login")).AsButton().Invoke();
Assert.NotNull(window.FindFirstDescendant(cf => cf.ByAutomationId("Welcome")));
}
}For per-test app launch (slower but isolates state), put Launch / Close in the test method itself; for per-class launch (faster but shared state), use IClassFixture (xUnit) / [OneTimeSetUp] (NUnit) / [ClassInitialize] (MSTest). Pair authoring conventions with dotnet-unit-tests (in the qa-unit-tests-net plugin) for the matching harness idioms.
STA threading
UIA3 (COM interop) requires an STA thread (msuia2 (opens in new window)); xUnit defaults to MTA, so set the apartment via the runner attribute:
// xUnit - install Xunit.StaFact and use [StaFact]
[StaFact]
public void Fact_running_on_sta_thread() { /* ... */ }
// NUnit - use [Apartment]
[Test, Apartment(ApartmentState.STA)]
public void Test_running_on_sta_thread() { /* ... */ }
// MSTest - STA is default; no attribute needed for sync testsUIA2 (managed) is more permissive, but keeping all UIA work on STA makes threading bugs easier to debug.
dotnet test invocation
:: Build + run
dotnet test --logger "trx;LogFileName=results.trx"
:: With a filter on the FlaUI smoke suite
dotnet test --filter "Category=Smoke" --logger "trx;LogFileName=smoke.trx"Verify: run the suite and confirm it launches the app and passes. If a test fails with a NullReference or Retry timeout, the locator or wait is wrong - open FlaUInspect (opens in new window) to recheck the AutomationId, fix the FindFirstDescendant / Retry call, then re-run before adding more cases.
Parsing results
xUnit / NUnit / MSTest emit standard TRX / JUnit XML output via the test logger flag. Pair with junit-xml-analysis (in the qa-test-reporting plugin) for cross-runner aggregation.
For interactive selector discovery during authoring, use FlaUInspect (opens in new window) - per its README it is "based on FlaUI" and presents the UIA tree with AutomationId, Name, ControlType, and XPath fields. Pre-built FlaUInspect.UIA2 and FlaUInspect.UIA3 binaries are downloadable from the releases page; pick the build matching the UIA mode used by the test project.
CI integration
Windows runner required (UIA is Windows-only per msuia2 (opens in new window)); use windows-latest for an interactive desktop, since UIA cannot drive Session-0. Full windows-latest workflow: references/flaui-api.md.
UIA2 vs UIA3 selection
Per flaui (opens in new window):
| Choose | When |
|---|---|
| UIA3 | WPF / Store Apps / new code - COM-based, fewer compatibility gaps with modern controls |
| UIA2 | Legacy WinForms / older Win32 - managed System.Windows.Automation (msuia2 (opens in new window)) handles some legacy controls UIA3 misses |
For new projects, UIA3 is the default recommendation (flaui (opens in new window)). UIA2 remains supported as a peer binding; FlaUI itself ships both packages.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Thread.Sleep(2000) between actions | Test runtime balloons; still flaky on slow CI | Use Retry.WhileNull / Retry.WhileTrue with explicit timeout per flauiretry (opens in new window) |
FindFirstByXPath("//Button[@Name='Save']") | Brittle to UI tree restructuring | Use ByAutomationId first; XPath only as last resort per flauisearch (opens in new window) |
Finding solely by visible Name (ByName) | Localised apps fail across languages | AutomationId is locale-independent per msuia2 (opens in new window) |
Sharing one Application across all test classes | UI state leaks between tests; one slow test halts the rest | Use one fixture per class (xUnit IClassFixture) |
Forgetting app.Dispose() / automation.Dispose() | Orphaned processes accumulate on CI runner | using declaration or IDisposable fixture |
Mouse-coordinate clicks (Mouse.Click(x, y)) | DPI / multi-monitor / theme changes break | Resolve element via UIA, call Invoke() |
| Asserting on raw bitmap screenshots | Brittle to font / theme / DPI | UIA tree is the assertion surface; screenshots only for canvas-rendered surfaces |
| Mixing UIA2 and UIA3 in one process | Unsupported per FlaUInspect (opens in new window) inspector constraint | Pick one binding per test project |
Limitations
References
flaui-tests - locator forms and CI reference
View source (opens in new window)flaui-tests - locator forms and CI reference
Alternate locator forms and the CI workflow, kept out of the SKILL spine. See SKILL.md (opens in new window) for the core launch, find, interact, and wait flow.
Sources: FlaUI Searching wiki (opens in new window), Microsoft Learn - UI Automation Overview (opens in new window).
Alternate locator forms
The lambda form (in SKILL.md) is the shortest and the upstream convention. Two equivalent forms resolve to the same UIA query:
// ConditionFactory form
var b = window.FindFirstDescendant(ConditionFactory.ByAutomationId("LoginButton"));
// Property + tree-scope form
var b3 = window.FindFirst(
TreeScope.Descendants,
new PropertyCondition(
Automation.PropertyLibrary.Element.AutomationIdProperty, "LoginButton"));Find method families (flauisearch (opens in new window))
Condition constructors: ByAutomationId, ByName, ByText, ByClassName, ByControlType, plus boolean combinators AndCondition / OrCondition / NotCondition.
Locator-selection order (most stable first)
CI integration
Windows runner required - UIA is Windows-only per msuia2 (opens in new window). windows-latest provides an interactive desktop session by default, required because UIA cannot drive Session-0 / non-interactive desktops. Self-hosted Windows-container runners need interactive logon + Auto-Login + an unlocked desktop.
# .github/workflows/flaui.yml
jobs:
ui-tests:
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-dotnet@v4
with: { dotnet-version: '8.0.x' }
- name: Build app under test
run: dotnet build src/MyApp -c Release
- name: Run FlaUI tests
run: dotnet test tests/MyApp.UiTests --logger "trx;LogFileName=ui.trx"
- uses: actions/upload-artifact@v4
if: always()
with:
name: trx-results
path: '**/ui.trx'Related skills
desktop-test-strategy-reference
Reference catalog of desktop GUI test strategies across Windows, macOS, and Linux. Defines the three accessibility-tree backends (Microsoft UI Automation on Windows, Apple Accessibility / XCTest on macOS, AT-SPI on Linux), the wrapper-tools that drive each backend, the cross-toolkit Electron + Qt paths, the project-marker detection table plus one-driver-per-app decision table (FlaUI / WinAppDriver / electron-playwright / QtTest / XCUITest / AT-SPI), an accessibility-first locator strategy, and a desktop test-review hazard checklist (screen-object encapsulation, locator stability, explicit waits, STA / foreground-lock / elevation). Deep operational detail (per-OS async-wait hierarchies, parallel-test policy, UAC / TCC / AT-SPI elevation hazards, the high-DPI matrix) lives in references/. Use when choosing how to test or automate a desktop GUI application on Windows, macOS, or Linux, or when reviewing an existing desktop UI test suite - the strategic reference ahead of the per-tool implementation skills.
electron-playwright
Authors Playwright `_electron` tests for packaged Electron desktop apps - launches the app via `electron.launch({ args })`, returns an `ElectronApplication` handle, drives renderer windows as Playwright `Page` objects, and probes the main process via `electronApp.evaluate(({ app, BrowserWindow }) => …)`. Distinct from ordinary browser page automation: this wraps the `_electron` API for launching packaged Electron apps and probing main + renderer processes. Includes the legacy Spectron reference and Spectron-to-Playwright migration shopping list (references/spectron-migration.md). Use for end-to-end tests of Electron apps where main-process state, IPC, and renderer DOM must all be asserted from one suite, or when migrating a deprecated Spectron suite.
qt-test-framework
Authors and runs Qt Test - the first-party C++ unit + GUI test framework that ships with Qt 6 (via the `QtTest` module header). Covers the `QTEST_MAIN` / `QTEST_APPLESS_MAIN` / `QTEST_GUILESS_MAIN` entry-point macros, the `QObject` private-slot test pattern, `QVERIFY` / `QCOMPARE` / `QFETCH` assertions, GUI event simulation (`QTest::mouseClick`, `QTest::keyClick`, `QTest::touchEvent`), `QSignalSpy` for signal introspection, `QBENCHMARK` for performance regression, and the `-o file,junitxml` CI output. Use for in-process testing of Qt widgets, QObject signal/slot chains, and Qt Quick / QML application logic; for out-of-process Qt-app driving, use an OS-native accessibility driver instead.
winappdriver
Authors and runs Windows UI tests against the WinAppDriver UIA surface via both invocation paths - the direct Microsoft W3C-WebDriver service (installing + launching `WinAppDriver.exe` on `127.0.0.1:4723`, `app` / `platformName` / `appArguments` / `appTopLevelWindow` capabilities) and the actively-maintained Appium 2.x wrapper (`appium driver install windows`, `appium:` prefixed capabilities, `windows:` gestures, PowerShell prerun/postrun hooks). Covers UWP / WPF / WinForms / Win32 apps, `AccessibilityId` / `Name` / `ClassName` locators, and Windows-runner CI. Use when driving a native Windows app from a Selenium-style client (C#, Java, Python, Ruby, JS) - directly when no Appium install is wanted, via Appium when the stack already runs Appium for iOS / Android / Mac2; for a C#-only FlaUI client use flaui-tests, and to choose among Windows desktop drivers first use desktop-test-strategy-reference.