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` or the Appium proxy layer of `appium-windows-driver`.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
FlaUI is a .NET library for automated UI testing of Windows applications
(flaui) 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) and UIA3 (COM interop).
v5.0.0 released February 2025; MIT-licensed and actively maintained
(flaui).
FlaUI is a .NET library that links into the test process and calls UIA directly. By contrast:
winappdriver is a Microsoft-maintained
HTTP/JSON service that exposes a W3C-WebDriver endpoint on
127.0.0.1:4723; tests speak Selenium-style protocol over the wire
and the driver is language-agnostic.appium-windows-driver is an
Appium 2.x proxy that sits in front of WinAppDriver.exe and adds
gestures / multi-window helpers.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. Pick
appium-windows-driver when you want the Appium feature surface on
top of WinAppDriver.
dotnet test solution).AsButton(),
AsTextBox()) over WebDriver's stringly-typed locators.For cross-language test stacks (Java / Python / Ruby clients), use
winappdriver instead.
Per flaui, 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) |
FlaUI.UIA2 | Managed UIA binding using System.Windows.Automation (msuia2) - better legacy WinForms compatibility (flaui) |
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 which requires the
inspector mode to be picked at startup.
Per the FlaUI wiki - Application page:
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: "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.
The lambda form is the shortest and is the upstream convention (flauisearch):
var loginButton = window.FindFirstDescendant(cf => cf.ByAutomationId("LoginButton"));Prefer ByAutomationId (developer-set, locale- and theme-independent per
msuia2); 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.
Per flaui:
// 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 §Control Patterns).
Before v2.0.0 some Find methods auto-retried; that responsibility now falls to
the caller (flauiretry):
// 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).
Each returns a RetryResult carrying iteration count, duration, and
the last value - the test can assert on those metrics when
diagnosing slow-loading screens.
WaitWhileBusy blocks while the target process is busy; a null timeout means
infinite, and it returns true if the application went idle
(flauiappsrc):
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.
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
xunit-tests, nunit-tests, or mstest-tests
(in the qa-unit-tests-net plugin) for the matching harness idioms.
UIA3 (COM interop) requires an STA thread (msuia2); 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 to recheck the AutomationId, fix the
FindFirstDescendant / Retry call, then re-run before adding more cases.
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 - 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.
Windows runner required (UIA is Windows-only per msuia2); use
windows-latest for an interactive desktop, since UIA cannot drive Session-0.
Full windows-latest workflow:
references/flaui-api.md.
Per flaui:
| 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) handles some legacy controls UIA3 misses |
For new projects, UIA3 is the default recommendation (flaui). UIA2 remains supported as a peer binding; FlaUI itself ships both packages.
| 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 |
FindFirstByXPath("//Button[@Name='Save']") | Brittle to UI tree restructuring | Use ByAutomationId first; XPath only as last resort per flauisearch |
Finding solely by visible Name (ByName) | Localised apps fail across languages | AutomationId is locale-independent per msuia2 |
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 inspector constraint | Pick one binding per test project |
xctest-mac-desktop and
at-spi-linux.windows-latest GitHub
runners are interactive by default; self-hosted containers need
Auto-Login + an unlocked desktop.IRawElementProviderSimple are opaque to FlaUI (and to
every other UIA-backed driver). Add UIA support in the application
or fall back to image matching for those screens.winappdriver,
appium-windows-driver,
desktop-test-strategy-reference.xunit-tests, nunit-tests, mstest-tests.