The desktop testing gapLe manque des tests desktop

Web testing has a rich ecosystem — Selenium, Playwright, Cypress. But when the application is a native Windows desktop app (WPF, WinForms, Win32), most of those tools simply don't apply. FlaUI fills that gap: a .NET library built on top of Microsoft's UI Automation (UIA3) framework that lets you drive any Windows application the same way a screen reader would. Les tests web ont un riche écosystème — Selenium, Playwright, Cypress. Mais quand l'application est une app desktop Windows native (WPF, WinForms, Win32), la plupart de ces outils ne s'appliquent tout simplement pas. FlaUI comble ce manque : une bibliothèque .NET construite sur le framework UI Automation (UIA3) de Microsoft qui te permet de piloter n'importe quelle application Windows comme le ferait un lecteur d'écran.

FlaUI is open-source, actively maintained, and significantly more developer-friendly than its predecessor WinAppDriver or the raw UIA COM API. FlaUI est open-source, activement maintenu, et significativement plus agréable pour les développeurs que son prédécesseur WinAppDriver ou l'API COM UIA brute.

How UI Automation worksComment fonctionne UI Automation

Every Windows UI control exposes an AutomationElement — a structured representation of that control with properties (Name, AutomationId, ControlType) and interaction patterns (InvokePattern, ValuePattern, SelectionPattern...). FlaUI wraps these into a clean C# API. Chaque contrôle UI Windows expose un AutomationElement — une représentation structurée de ce contrôle avec des propriétés (Name, AutomationId, ControlType) et des patterns d'interaction (InvokePattern, ValuePattern, SelectionPattern...). FlaUI les enveloppe dans une API C# propre.

Pattern Used forUtilisé pour ExampleExemple
InvokePattern Click a buttonCliquer un bouton button.Invoke()
ValuePattern Read / set a text fieldLire / écrire un champ texte input.SetValue("text")
SelectionPattern Interact with list / comboboxInteragir avec liste / combobox item.Select()
TogglePattern Check / uncheck a checkboxCocher / décocher une case checkbox.Toggle()
WindowPattern Maximize, minimize, closeMaximiser, minimiser, fermer window.Close()

Inspecting elements — FlaUI InspectInspecter les éléments — FlaUI Inspect

Before writing a single line of code, you need to identify the elements you want to interact with. FlaUI Inspect (or the built-in Accessibility Insights tool) lets you hover over any UI control and see its AutomationId, Name, ControlType, and available patterns. Avant d'écrire une seule ligne de code, tu dois identifier les éléments avec lesquels tu veux interagir. FlaUI Inspect (ou l'outil Accessibility Insights intégré) te permet de survoler n'importe quel contrôle UI et de voir son AutomationId, Name, ControlType et les patterns disponibles.

💡 Priority order for selectors : AutomationId first (stable, set by dev) → Name (visible, but can change with localization) → ControlType + index (last resort, fragile). Always ask the development team to set meaningful AutomationIds on critical controls. 💡 Ordre de priorité pour les sélecteurs : AutomationId en premier (stable, défini par le dev) → Name (visible, mais peut changer avec la localisation) → ControlType + index (dernier recours, fragile). Toujours demander à l'équipe dev de définir des AutomationIds significatifs sur les contrôles critiques.

Setting up FlaUIMise en place de FlaUI

NuGet packages
# UIA3 is for modern apps (WPF, UWP, Win32 with UIA support)
# Use FlaUI.UIA2 for legacy WinForms if UIA3 has issues
dotnet add package FlaUI.UIA3
dotnet add package FlaUI.Core
dotnet add package NUnit
dotnet add package NUnit3TestAdapter

Writing tests with FlaUI + NUnitÉcrire des tests avec FlaUI + NUnit

The pattern is: launch the application, get the main window, find elements by their properties, interact, assert. A BaseTest class handles the lifecycle so each test gets a clean application instance. Le pattern est : lancer l'application, obtenir la fenêtre principale, trouver les éléments par leurs propriétés, interagir, asserter. Une classe BaseTest gère le cycle de vie pour que chaque test obtienne une instance propre de l'application.

BaseTest.cs
public class BaseTest
{
    protected Application App;
    protected UIA3Automation Automation;
    protected Window MainWindow;

    [SetUp]
    public void Setup()
    {
        Automation = new UIA3Automation();
        App = Application.Launch(@"C:\MyApp\MyApp.exe");
        MainWindow = App.GetMainWindow(Automation,
                         TimeSpan.FromSeconds(5));
    }

    [TearDown]
    public void TearDown()
    {
        App?.Close();
        Automation?.Dispose();
    }
}
LoginTest.cs
public class LoginTest : BaseTest
{
    [Test]
    public void ValidLogin_OpensDashboard()
    {
        // Find elements by AutomationId — most stable selector
        var usernameBox = MainWindow.FindFirstDescendant(
            cf => cf.ByAutomationId("txtUsername"))
            .AsTextBox();

        var passwordBox = MainWindow.FindFirstDescendant(
            cf => cf.ByAutomationId("txtPassword"))
            .AsTextBox();

        var loginBtn = MainWindow.FindFirstDescendant(
            cf => cf.ByAutomationId("btnLogin"))
            .AsButton();

        // Interact
        usernameBox.Text = "admin";
        passwordBox.Text = "secret123";
        loginBtn.Invoke();

        // Wait for dashboard window
        var dashboard = App.GetWindow(
            cf => cf.ByName("Dashboard"), Automation,
            TimeSpan.FromSeconds(5));

        Assert.That(dashboard, Is.Not.Null);
        Assert.That(dashboard.Title, Does.Contain("Dashboard"));
    }
}

Page Object pattern for desktopPattern Page Object pour le desktop

The same POM principle from web testing applies perfectly here. Each screen or dialog gets its own class that encapsulates element lookup and actions — tests stay readable and maintenance stays localized. Le même principe POM des tests web s'applique parfaitement ici. Chaque écran ou dialogue obtient sa propre classe qui encapsule la recherche d'éléments et les actions — les tests restent lisibles et la maintenance reste localisée.

pages/LoginScreen.cs
public class LoginScreen
{
    private readonly Window _window;

    private TextBox Username => _window
        .FindFirstDescendant(cf => cf.ByAutomationId("txtUsername")).AsTextBox();

    private TextBox Password => _window
        .FindFirstDescendant(cf => cf.ByAutomationId("txtPassword")).AsTextBox();

    private Button LoginButton => _window
        .FindFirstDescendant(cf => cf.ByAutomationId("btnLogin")).AsButton();

    public LoginScreen(Window window) => _window = window;

    public void LoginAs(string user, string pass)
    {
        Username.Text = user;
        Password.Text = pass;
        LoginButton.Invoke();
    }
}

Handling timing — the desktop challengeGérer le timing — le défi du desktop

Desktop apps don't have a network request you can wait on. UI transitions, loading dialogs, and background processing all require explicit waiting strategies. Les apps desktop n'ont pas de requête réseau sur laquelle attendre. Les transitions UI, les dialogues de chargement et le traitement en arrière-plan nécessitent des stratégies d'attente explicites.

waiting strategies
// ① Wait for element to exist
var element = MainWindow.FindFirstDescendant(
    cf => cf.ByAutomationId("loadingSpinner"));
element.WaitUntilEnabled(TimeSpan.FromSeconds(10));

// ② Poll until condition is met
Retry.WhileFalse(
    () => MainWindow.FindFirstDescendant(
              cf => cf.ByAutomationId("btnSave"))?
          .IsEnabled == true,
    TimeSpan.FromSeconds(5),
    throwOnTimeout: true
);

// ③ Wait for a new window to appear
var dialog = Retry.Find(
    () => App.GetAllTopLevelWindows(Automation)
             .FirstOrDefault(w => w.Title.Contains("Confirm")),
    TimeSpan.FromSeconds(5)
);

⚠️ Never use Thread.Sleep() in desktop tests. It makes tests slow and still doesn't guarantee stability. Use Retry.WhileFalse() or WaitUntilEnabled() instead. ⚠️ Ne jamais utiliser Thread.Sleep() dans les tests desktop. Ça rend les tests lents sans garantir la stabilité. Utiliser Retry.WhileFalse() ou WaitUntilEnabled() à la place.

Running in CI on WindowsExécution en CI sur Windows

FlaUI requires a real Windows session with a visible desktop — standard Linux containers won't work. GitHub Actions provides windows-latest runners with a full GUI session, which is exactly what we need. FlaUI nécessite une vraie session Windows avec un bureau visible — les containers Linux standard ne fonctionneront pas. GitHub Actions fournit des runners windows-latest avec une session GUI complète, ce qui est exactement ce qu'il faut.

.github/workflows/flaui-tests.yml
name: FlaUI Desktop Tests
on: [push]

jobs:
  desktop-tests:
    runs-on: windows-latest   # REQUIRED — needs a real Windows session
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'
      - name: Build app under test
        run: dotnet build src/MyApp/MyApp.csproj -c Release
      - name: Run FlaUI tests
        run: dotnet test tests/MyApp.UITests --logger trx
      - name: Publish test results
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: FlaUI Results
          path: '**/*.trx'
          reporter: dotnet-trx

Key takeawaysCe qu'il faut retenir

  • 🆔
    AutomationId is kingL'AutomationId est roi
    Push your dev team to set AutomationIds on all interactive controls from the start. It's the most stable and readable selector — infinitely better than XPath or index-based lookups. Pousser l'équipe dev à définir des AutomationIds sur tous les contrôles interactifs dès le départ. C'est le sélecteur le plus stable et lisible — infiniment mieux que XPath ou les lookups par index.
  • 🏗️
    POM works for desktop tooLe POM fonctionne aussi pour le desktop
    One class per screen. Tests describe behavior, screens describe interaction. The structure is identical to web testing. Une classe par écran. Les tests décrivent le comportement, les écrans décrivent l'interaction. La structure est identique aux tests web.
  • ⏱️
    Timing is the hardest partLe timing est la partie la plus difficile
    Desktop apps have no network requests to intercept. Master Retry and WaitUntil early — they are your main tools against flakiness. Les apps desktop n'ont pas de requêtes réseau à intercepter. Maîtriser Retry et WaitUntil tôt — ce sont tes principaux outils contre l'instabilité.
  • 🖥️
    Windows runner for CIRunner Windows pour la CI
    FlaUI needs a visible desktop session. Always use windows-latest in GitHub Actions — never a Linux or Docker container. FlaUI a besoin d'une session de bureau visible. Toujours utiliser windows-latest dans GitHub Actions — jamais un container Linux ou Docker.