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
# 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.
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(); } }
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.
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.
// ① 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.
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. MasterRetryandWaitUntilearly — they are your main tools against flakiness. Les apps desktop n'ont pas de requêtes réseau à intercepter. MaîtriserRetryetWaitUntiltôt — ce sont tes principaux outils contre l'instabilité. -
Windows runner for CIRunner Windows pour la CI
FlaUI needs a visible desktop session. Always usewindows-latestin GitHub Actions — never a Linux or Docker container. FlaUI a besoin d'une session de bureau visible. Toujours utiliserwindows-latestdans GitHub Actions — jamais un container Linux ou Docker.