Skip to content
End To End Tester
Platformspractical

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.

csharp
// 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:

xml
<!-- 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
// 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.

python
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

Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.

Was this page useful?