Skip to content

Test Windows native apps

Use case: part of your product is a Windows desktop application — a point-of-sale client, a device-configuration tool, an installer, a legacy WPF admin app. No browser, no DOM, and none of your web tooling applies. It still needs regression tests.

▶ Runnable sample: examples/windows-flaui — how to run: examples/README

  • Playwright, Selenium and friends drive browsers. A native window has no DOM — instead Windows exposes a UI Automation (UIA) tree of elements.
  • The classic tool, WinAppDriver, is effectively unmaintained; teams keep building on a dead end.
  • Elements are only findable if developers set AutomationIds — the data-testid of the Windows world — and most codebases haven’t.
  • CI is unforgiving: UIA needs a real, unlocked, interactive desktop session; a locked screen makes every click fail.

FlaUI is the actively maintained .NET library over UIA3 — the most robust choice when your app (or your team) is .NET:

using FlaUI.Core;
using FlaUI.UIA3;
using var app = Application.Launch(@"C:\app\PosClient.exe");
using var automation = new UIA3Automation();
var window = app.GetMainWindow(automation);
window.FindFirstDescendant(cf => cf.ByAutomationId("AmountInput"))
.AsTextBox().Enter("1980");
window.FindFirstDescendant(cf => cf.ByAutomationId("ChargeButton"))
.AsButton().Invoke();
var receipt = window.FindFirstDescendant(cf => cf.ByAutomationId("ReceiptView"));
Assert.Contains("¥1,980", receipt.AsLabel().Text);

Run it with any .NET test framework (xUnit/NUnit) — you get normal test tooling, retries and reports.

If your test stack is JS/Python and you want one Appium roof over mobile and desktop, the Appium Windows Driver drives the same UIA layer:

const driver = await remote({
capabilities: {
platformName: 'Windows',
'appium:automationName': 'Windows',
'appium:app': 'C:\\app\\PosClient.exe', // or an AppUserModelID for Store apps
},
});
await driver.$('~AmountInput').setValue('1980'); // ~ = accessibility id = AutomationId
await driver.$('~ChargeButton').click();

(For quick Python scripts, pywinauto with backend="uia" covers the same ground.)

  • Inspect the UIA tree with Accessibility Insights for Windows — it’s the devtools inspector for native apps.
  • If elements show up nameless, add AutomationProperties.AutomationId in XAML (or Control.Name in WinForms). This is the single highest-leverage change; it also improves screen-reader accessibility for free.
  • Custom-drawn controls (owner-drawn grids, game-like canvases) have no UIA presence — fall back to pixel techniques: screenshots + tolerant comparison, exactly as in canvas testing.

UIA input requires an unlocked interactive desktop:

  • Self-hosted Windows VM with auto-logon and screen lock disabled is the reliable setup.
  • On shared runners, keep the session alive (tscon-to-console trick) or run inside an RDP session that stays connected.
  • Record a screen capture per run (ffmpeg -f gdigrab …) — it’s your trace viewer when a click lands in the void.
  • Avoid coordinate-based clicking; DPI scaling and theme changes break it. UIA elements + Invoke/patterns survive UI facelifts.
  • WebView2/Electron hybrids: automate the web part with Playwright (attach via CDP) and only the native shell via UIA — don’t drive HTML through UIA, the tree is miserable.
  • Dialogs from the OS (file pickers, UAC) live outside your app’s process; find them from the desktop root, and disable UAC prompts on test machines.