Desktop Application Testing
Automating WPF, WinUI, WinForms, Electron and native desktop apps — the accessibility trees, the tooling, and why desktop UI automation is harder than the web.
2 min read · updated 19 September 2026
Desktop automation works the same way web automation does — find an element in a tree, act on it, assert — but every part of that is harder, because the tree is an afterthought in most desktop frameworks and the tooling is thinner.
#The accessibility trees
| Platform | Tree | Automate with |
|---|---|---|
| Windows (WPF, WinForms, WinUI, Win32) | UI Automation | FlaUI, Appium Windows Driver |
| macOS | NSAccessibility | XCUITest, Appium Mac2 driver |
| Linux | AT-SPI | Dogtail, ldtp |
| Electron | the DOM | Playwright |
| Avalonia | its own headless platform | Avalonia.Headless |
Electron is by far the easiest, because it is a browser. If you are choosing a desktop stack and testability matters, that is a real consideration.
#Windows: FlaUI
For .NET, talking to UI Automation directly is better than going through WebDriver — faster, better typed, no server process.
// C#, FlaUI + xUnit
public class CheckoutTests : IDisposable
{
private readonly Application _app;
private readonly UIA3Automation _automation = new();
private readonly Window _window;
public CheckoutTests()
{
_app = Application.Launch("bin/Release/net9.0-windows/Shop.exe");
_window = _app.GetMainWindow(_automation, TimeSpan.FromSeconds(30));
}
[Fact]
public void Paying_shows_a_confirmation()
{
// AutomationId is the desktop equivalent of a test id. Set it in XAML.
_window.FindFirstDescendant(cf => cf.ByAutomationId("CardNumber"))
.AsTextBox().Text = "4242424242424242";
_window.FindFirstDescendant(cf => cf.ByAutomationId("Pay")).AsButton().Invoke();
// Nothing waits for you: Retry is the whole waiting model.
var confirmation = Retry.WhileNull(
() => _window.FindFirstDescendant(cf => cf.ByAutomationId("Confirmation")),
timeout: TimeSpan.FromSeconds(10)).Result;
Assert.Equal("Order confirmed", confirmation.AsLabel().Text);
}
public void Dispose() { _app.Close(); _automation.Dispose(); }
}Retry.WhileNull is doing the job that
Playwright does automatically on the web. Every desktop
suite ends up with a helper like it, and the quality of that helper largely
determines how flaky the suite is.
#Make the app findable
The single highest-return change is setting automation ids in the UI definition:
<!-- WPF / WinUI -->
<Button x:Name="PayButton"
AutomationProperties.AutomationId="Pay"
AutomationProperties.Name="Pay £27.95"
Content="Pay" />
<TextBox AutomationProperties.AutomationId="CardNumber"
AutomationProperties.LabeledBy="{Binding ElementName=CardLabel}" />Without these you are matching on control type and position, which breaks on every layout change. With them, the suite is stable and the app is more accessible — the same dual benefit as accessibility ids in mobile.
#Electron
// TypeScript, Playwright. A desktop app tested like a web page.
import { _electron as electron } from 'playwright';
test('creates a document and saves it', async () => {
const app = await electron.launch({ args: ['.'] });
const window = await app.firstWindow();
await window.getByRole('button', { name: 'New document' }).click();
await window.getByRole('textbox', { name: 'Title' }).fill('Notes');
await window.keyboard.press('Control+S');
await expect(window.getByRole('status')).toHaveText('Saved');
// The main process is reachable too — assert on what it actually did.
const savedPath = await app.evaluate(async ({ app }) => app.getPath('documents'));
expect(savedPath).toBeTruthy();
await app.close();
});app.evaluate runs in the Electron main process, so you can assert on menu
state, file paths and IPC — things no browser test can reach.
#Appium Windows Driver
The cross-language option, and the successor to the archived WinAppDriver.
from appium import webdriver
driver = webdriver.Remote("http://127.0.0.1:4723", options=WindowsOptions().load_capabilities({
"platformName": "Windows",
"appium:automationName": "Windows",
"appium:app": r"C:\Program Files\Shop\Shop.exe",
}))
driver.find_element("accessibility id", "CardNumber").send_keys("4242424242424242")
driver.find_element("accessibility id", "Pay").click()Slower than FlaUI and worth it if your team already writes Appium tests for mobile and wants one vocabulary.
#What to test where
Desktop UI tests are the most expensive tests in this whole reference: slow, environment-sensitive, and requiring a real session with a real display. The pyramid argument applies with more force here than anywhere.
- View models and services — plain unit tests, no UI. In MVVM this is most of the application and it needs no automation at all.
- Integration — the real database, the real file system, no window.
- UI — a handful of journeys, and only the ones where the window itself is the risk.
If you are choosing a framework today and testability matters, Avalonia is worth a look: its headless test platform runs real UI tests in CI in milliseconds with no display server, which is something no other desktop stack here offers.
Common questions
- Why is desktop UI automation harder than web automation?
- The accessibility tree is usually an afterthought rather than the primary interface, there is no equivalent of the browser's developer tools, the frameworks are older and more varied, and there is no auto-waiting anywhere. You end up writing the waiting logic that Playwright gives the web for free.
- What replaced WinAppDriver?
- Microsoft archived WinAppDriver in 2022; the Appium Windows Driver continues it and is the maintained path. For .NET specifically, FlaUI talks to UI Automation directly and is usually a better experience than going through WebDriver at all.
- How do I test an Electron app?
- Playwright has first-class Electron support — it launches the app and gives you a normal page object for each window. That makes Electron by far the easiest desktop target, because it is a browser.
Runnable samples for this page
- reference only
reference/platforms/desktop-testingreference only
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Testing Avalonia ApplicationsAvalonia's headless test platform runs real UI tests in CI in milliseconds with no display server — the most testable desktop stack in .NET.
- AppiumOne WebDriver-based API across iOS and Android — how the drivers map onto XCUITest and UIAutomator, locator strategies, and the cost of cross-platform tests.
- The Page Object ModelEncapsulating a screen behind a class so tests describe intent rather than selectors — what it fixes, the god-object failure mode, and modern alternatives.
- End-to-End TestingWhat belongs in an end-to-end suite and what does not, how many journeys are enough, and the practices that keep a browser suite from becoming the thing everyone ignores.
- Writing Tests in C#The .NET testing stack — xUnit, Moq or NSubstitute, WebApplicationFactory, Testcontainers — and the dependency injection story that makes it the most testable of the four.
- PlaywrightThe default browser automation tool in 2026 — auto-waiting locators, tracing, sharding and fixtures — with the configuration that matters and the mistakes that still cause flakiness.