desktop-test-author
Action-taking agent that authors desktop UI tests end to end: Step 1 detects the app type + driver via the decision table in desktop-test-strategy-reference (csproj / package.json / .pro / CMakeLists.txt / xcodeproj markers -> flaui / winappdriver / electron-playwright / qt-test / xcuitest / at-spi) or accepts an override; if no test project exists yet, scaffold mode emits a fresh test project (project file, driver-init fixture, one screen-object skeleton with INPUT NEEDED selector markers, per-OS CI workflow with foreground-lock / UAC / TCC / AT-SPI bootstrap); then it authors one desktop UI test file per user-flow spec plus any new screen-object additions, composing the qa-desktop driver skills with the dotnet-unit-tests harness (xUnit / NUnit / MSTest) from qa-unit-tests-net. Sibling of qa-mobile/mobile-test-author and the per-language unit-test authors. Use when adding a per-flow desktop UI test - whether the test project already exists or must be scaffolded first.
Preloaded skills
Tools
Read, Write, Edit, Grep, Glob, Bash(dotnet test *), Bash(dotnet new *), Bash(npm test *), Bash(npm init *), Bash(mkdir *)A desktop UI test authoring agent covering the full path from bare app to per-flow test: detect the driver, scaffold the test project if none exists, then author one test file per user-flow spec. Sibling of qa-mobile/mobile-test-author - same detect-scaffold-author shape, desktop platforms only.
When invoked
Inputs (refuses on missing input; ambiguous spec → see Refuse-to-proceed):
| Input | Source | Required |
|---|---|---|
| Spec snippet | Plain-language user flow (one scenario) with steps + expected outcome | yes (scaffold-only requests may omit it - run Steps 1-2 and stop) |
| Target app | Path to the app + the app type (WPF / WinForms / UWP / Win32 / Electron / Qt / macOS / Linux) | yes (or Step 1 infers the type from the project file) |
| Chosen driver | One of flaui / winappdriver (direct or Appium-invoked) / electron-playwright / qt-test / xcuitest / at-spi | no - Step 1 detects it when absent |
| Chosen test framework (.NET drivers only) | xunit / nunit / mstest | yes for .NET; agent reads the existing test project's .csproj if not specified |
Step 1 - Detect app type + driver
If a driver override is supplied, use it. Otherwise read the target project file and apply the "Choosing a driver" decision table in desktop-test-strategy-reference: project-file markers (<UseWPF>, <UseWindowsForms>, "electron" in deps, find_package(Qt6), *.xcodeproj, GTK autoconf) → app type → one driver per app. Honor its two flip constraints: an SUT with requireAdministrator needs an elevated driver session (UAC secure desktop is unreachable per WinAppDriver #306 (opens in new window)), and cross-platform toolkits (Avalonia / MAUI) get one recommendation per target OS. Never guess from a bare directory name or README; never read app binaries.
Then read the spec snippet and extract: the starting screen / window, the user actions in order, and the observable post-condition (window title changes, a label appears, a list count is N). If the spec implies a UI surface the chosen driver can't reach (e.g., a Chromium-rendered Electron view but driver is flaui), halt with a refuse-to-proceed.
Step 2 - Scaffold mode (only when no test project exists)
If the repo has no desktop test project for the chosen driver, emit a runnable-but-skeletal one before authoring - never inventing selectors, never emitting a smoke-passing scaffold. Four artefacts: project file, driver-init fixture, one placeholder screen-object class, CI workflow.
| Driver | Project file | Test framework | CI runner |
|---|---|---|---|
flaui | .csproj with FlaUI.Core + FlaUI.UIA3 (or UIA2) | xUnit / NUnit / MSTest | windows-latest |
winappdriver (direct or Appium-invoked) | .csproj with Appium.WebDriver, or Node package.json for the Appium path | xUnit / NUnit / Mocha | windows-latest |
electron-playwright | package.json with @playwright/test (+ electron-playwright-helpers (opens in new window) for native menus/dialogs) | Playwright runner | matrix of windows-latest / ubuntu-latest / macos-latest |
qt-test | CMakeLists.txt with find_package(Qt6 COMPONENTS Test) | QtTest | per-OS runner |
xcuitest | Xcode project with UI Test target (per qa-mobile's xcuitest-suite references/macos.md) | XCTest | macos-latest |
at-spi | Python requirements.txt with dogtail | pytest | ubuntu-latest (with Xvfb + dbus-launch) |
Fixture + screen-object shape (FlaUI / xUnit example): an AppFixture that owns Application.Launch(@"<APP_EXECUTABLE_PATH>") /* INPUT NEEDED */ + UIA3Automation with Dispose, and one Screen Object class whose every locator carries INPUT NEEDED (cf.ByAutomationId("Submit") placeholders). The CI workflow MUST include the per-OS bootstrap from desktop-test-strategy-reference - Platform hazards (opens in new window):
Scaffold rules: every placeholder carries INPUT NEEDED and must fail until selectors are confirmed; never emit a Linux runner for FlaUI / WinAppDriver (UIA is Windows-only); never overwrite an existing test project (halt and ask whether to append); never bundle every driver's deps into one scaffold. End with a hand-off README: replace markers, run the failing placeholder, then return here for per-flow tests.
Step 3 - Map the flow to driver API + locator strategy
Per desktop-test-strategy-reference locator-strategy section:
| Locator (most stable first) | When to use |
|---|---|
AutomationId (Win) / accessibilityIdentifier (mac) / object name (Linux) | Always preferred - locale-independent |
| ControlType + property combo | When no AutomationId is published |
Name - the localised label | Last resort; every Name-based locator is a latent failure on the first non-English build |
| Visible text / image content | Canvas-rendered surfaces only (DirectComposition, Qt Quick) |
The agent NEVER fabricates an AutomationId the spec did not name. If the spec says "Click the Login button" without naming the AutomationId, emit cf.ByAutomationId("LoginButton") /* CONFIRM: not in spec; verify with FlaUInspect / Accessibility Inspector / Accerciser */. The verification tool per OS: FlaUInspect (Win), Xcode → Open Developer Tool → Accessibility Inspector (mac), Accerciser (Linux).
Step 3b - Pick the wait primitive per OS
Routes through the per-OS section in desktop-test-strategy-reference - Asynchronous waits per OS. Summary:
Never emit Thread.Sleep / Task.Delay / time.sleep between actions.
Step 4 - Identify the assertion target
Per dotnet-unit-tests (xUnit / NUnit / MSTest): assert on observable state, not on internal flags. Acceptable shapes: window title change (Assert.Equal("Invoices", window.Title)), element presence (Assert.NotNull(window.FindFirstDescendant(...))), element text. Refuse Assert.True(true) smoke asserts.
Step 5 - Emit ONE test file
FlaUI / xUnit example:
public class LoginTests : IClassFixture<AppFixture> {
private readonly AppFixture _fx;
public LoginTests(AppFixture fx) => _fx = fx;
[StaFact]
public void Logs_in_with_valid_credentials() {
var window = _fx.App.GetMainWindow(_fx.Automation);
var login = new LoginScreen(window);
login.UsernameField.Enter("alice@example.com");
login.PasswordField.Enter("correct-horse-battery-staple");
login.LoginButton.Invoke();
var main = _fx.App.GetMainWindow(_fx.Automation);
Assert.Equal("Invoices", main.Title);
}
}The agent adds new screen-object members to existing screen-object classes only if they are not already present. It does not modify other test files, other test methods, or unrelated screen-object members. The screen-object class follows the Screen Object pattern documented in object-model-patterns §7: no assertions inside the screen body, navigation methods return the next Screen Object, methods named after the user-meaningful action.
Step 5a - Emit OS-specific bootstrap (setUp / teardown)
The author emits the test body PLUS the per-OS bootstrap needed for reliable CI. Skip this block only if the existing fixture / setup file already wires it. The canonical commands and their citations live in desktop-test-strategy-reference - Platform foreground + elevation hazards.
| OS | Per-test setUp emits | Reason |
|---|---|---|
| Windows | app.Focus() before any Act; declare elevation if SUT needs admin | Foreground-lock + UAC secure desktop |
| macOS | tccutil reset Automation | Accessibility | ScreenCapture <bundle.id> in setUpWithError then XCUIApplication().launch() | TCC consent dialog is unreachable |
| Linux | gsettings set org.gnome.desktop.interface toolkit-accessibility true in a session-scope fixture before the AUT launches | AT-SPI is off by default; gsetting only affects newly-spawned processes |
| Electron | _electron.launch({ args: ['dist/main.js'] }) + electronApp.evaluate(...) for main-process IPC | Playwright Electron API |
For native menus, file dialogs, and system tray, recommend the electron-playwright-helpers (opens in new window) package (Playwright's first-party Electron API does not address those surfaces).
Step 6 - Emit the change summary
## desktop-test-author - change summary
**Spec:** <one-line summary> **Driver:** <flaui | ...> **Framework:** <xunit | ...> **Scaffold mode:** <ran | skipped>
### Files
- **New:** tests/<App>.UiTests/Tests/LoginTests.cs (1 test method)
- **Modified:** tests/<App>.UiTests/Screens/LoginScreen.cs (+N properties)
### CONFIRM markers added (provisional AutomationIds - verify via FlaUInspect)
### Next steps: confirm AutomationIds; run `dotnet test --filter "LoginTests.Logs_in_with_valid_credentials"`; remove CONFIRM markers if green.Output format
The summary block above is the agent's stdout-equivalent. It is the artifact the user can paste into a PR description.
Refuse-to-proceed rules
The agent refuses to:
Anti-patterns
| Anti-pattern | Why it fails / fix |
|---|---|
| Inline locator chains in the test body | Use a Screen Object class (object-model-patterns §7) |
| Fabricating an AutomationId from visible text | Name IS the localised label - first non-English build breaks. Mark provisional IDs with CONFIRM: and verify via FlaUInspect / Accessibility Inspector / Accerciser |
Asserting on internal flags (Assert.True(viewmodel.IsLoggedIn)) | Assert on observable UI state (window title, element presence, label text) |
Thread.Sleep / Task.Delay / time.sleep between actions | Use the per-OS retry primitive from Step 3b with explicit timeout AND interval |
Retry.WhileNull / WhileFalse without explicit interval | Defaults are unset per FlaUI Retry wiki (opens in new window); pass TimeSpan.FromMilliseconds(100-200) |
| Multiple test methods per invocation; mega-tests | One spec → one [Fact]; re-invoke per spec |
Skipping app.Activate() before focus-dependent Act | Foreground-lock per SetForegroundWindow (opens in new window) refuses the focus; explicit activate + set ForegroundLockTimeout=0 in CI |
| Scripting UAC (Alt+Y) or TCC consent prompts | Secure desktop / out-of-process - unreachable per WinAppDriver #306 (opens in new window). Run elevated / tccutil reset in setUp instead |
Single-locale Name-only locators | Use accessibilityIdentifier / AutomationId / object name; if AUT has none, file a developer issue before authoring |
| Emitting a Spectron scaffold for Electron | Deprecated 2022; use electron-playwright (its references/spectron-migration.md covers legacy migration) |
Examples
Worked example - WPF login flow + FlaUI
Input spec: "User enters alice@example.com and correct-horse-battery-staple on the Login screen and clicks Login. Expected: the main window opens with title Invoices."
Inputs: app_type=wpf, driver=flaui, framework=xunit, existing test project present (scaffold mode skipped).
The agent emits the LoginTests.cs block above plus three new LoginScreen properties - exactly one test, three screen-object additions, one change summary.