$npx -y skills add microsoft/win-dev-skills --skill winui-ui-testingAutomated UI testing for Windows desktop apps — generate a batch test script with the winapp ui UI Automation harness, run all tests in one pass, read results. Covers element assertions, interactions, value checking (TextBox, ComboBox, ToggleSwitch), keyboard shortcuts and typi
| 1 | ### Scope — any Windows app |
| 2 | |
| 3 | `winapp ui` drives Windows **UI Automation (UIA)**, the accessibility layer every Windows UI framework exposes, so the AutomationId-based approach in this skill works on **any** Windows desktop app: Win32, WPF, WinForms, and WinUI 3, packaged or unpackaged. The file-picker tests below already drive the OS's Win32 file dialog through the same verbs. For a non-WinUI app, use the same verbs and script template and skip the WinUI-specific gotchas (x:Bind `LostFocus` commit, ContentDialog selectors, MSIX relaunch). |
| 4 | |
| 5 | ### Approach |
| 6 | |
| 7 | The goal of this skill is to validate UI and app functionality automatically, without manual interaction, by exercising the app's UI elements, verifying their state, and asserting that the app behaves as expected under test conditions. |
| 8 | |
| 9 | There are two main approaches: |
| 10 | 1. Interactive exploration — manually run the app, use `winapp ui <command>` to explore the UI tree, find AutomationIds, verify element properties, and test functionality interactively. This is useful for discovery, but slow and expensive if repeated for every test iteration. |
| 11 | 2. Scripted batch testing — generate a `ui-tests.ps1` script that exercises all UI elements and asserts expected behavior in one pass. This allows you to run the tests automatically, capture results, and iterate quickly without manually interacting with the app each time. |
| 12 | |
| 13 | Unless the user asked for interactive exploration, or you are unfamiliar with the code/app or need to explore the UI tree to discover AutomationIds for hidden or dynamically generated elements (flyouts, dialogs, lazy-loaded content), **prefer scripted batch testing** — it is faster, repeatable, and produces a record of pass/fail results that can be reviewed and acted on. |
| 14 | |
| 15 | ### `winapp ui` Verbs |
| 16 | |
| 17 | - **Query:** `status`, `list-windows`, `inspect`, `search`, `get-property`, `get-value`, `get-focused`, `wait-for` |
| 18 | - **Interact:** `invoke`, `click`, `set-value`, `focus`, `scroll`, `scroll-into-view` |
| 19 | - **Advanced input:** `send-keys` (synthetic keyboard + accelerators), `hover` (tooltips/flyouts), `drag` (drag-drop, reorder, sliders), `touch` (tap/swipe/pinch/stretch), `pen` (stylus ink, pressure/tilt/eraser) |
| 20 | - **Capture:** `screenshot`, `record` (H.264 MP4 video) |
| 21 | |
| 22 | Run `winapp ui --cli-schema` for the complete command structure as JSON, or `winapp ui <verb> --help` for any single verb. |
| 23 | |
| 24 | ### Step 1: Use the Running App |
| 25 | |
| 26 | If the app is already running, use its PID. **Do NOT relaunch** — use the PID already captured from the build step. If the app is not running, build and launch it using the guidance in the winui-dev-workflow skill. |
| 27 | |
| 28 | ### Step 2: Write the Test Script |
| 29 | |
| 30 | **If you wrote the code:** Skip inspect — you already know all the AutomationIds and control structure from the XAML and code-behind. Write tests directly from that knowledge. Inspect misses popups, flyouts, dialogs, and lazy-loaded content anyway. |
| 31 | |
| 32 | **If you're verifying code you didn't write:** Run inspect first to discover the UI: |
| 33 | ```powershell |
| 34 | winapp ui inspect -a <PID> --interactive |
| 35 | ``` |
| 36 | Then read the XAML files to find AutomationIds that aren't currently visible (flyout items, dialog buttons, secondary pages). |
| 37 | |
| 38 | Create a `ui-tests.ps1` file that tests all the app's requirements in one pass: |
| 39 | |
| 40 | ```powershell |
| 41 | # ui-tests.ps1 |
| 42 | param([Parameter(Mandatory)][int]$AppPid) |
| 43 | # NOTE: Do NOT name the parameter $Pid — it's read-only in PowerShell |
| 44 | |
| 45 | $ErrorActionPreference = 'Continue' |
| 46 | $pass = 0; $fail = 0; $results = @() |
| 47 | |
| 48 | # Get main window HWND (avoids PopupHost interference with JSON parsing) |
| 49 | $windows = winapp ui list-windows -a $AppPid --json 2>$null | ConvertFrom-Json |
| 50 | $hwnd = ($windows | Where-Object { $_.title -ne "PopupHost" } | Select-Object -First 1).hwnd |
| 51 | |
| 52 | function Test-UI { |
| 53 | param([string]$Name, [scriptblock]$Script) |
| 54 | # IMPORTANT: Inside $Script, use 'throw' to signal failure — NOT 'exit 1' |
| 55 | # (exit terminates the entire script, not just the test) |
| 56 | try { |
| 57 | $output = & $Script 2>&1 |
| 58 | if ($LASTEXITCODE -eq 0) { |
| 59 | $script:pass++; $script:results += @{ name = $Name; status = "PASS" } |
| 60 | } else { |
| 61 | $script:fail++; $script:results += @{ name = $Name; status = "FAIL"; detail = "$output" } |
| 62 | } |
| 63 | } catch { |
| 64 | $script:fail++; $script:results += @{ name = $Name; status = "FAIL"; detail = "$_" } |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | # ─── Element Existence ─── |
| 69 | Test-UI "NavHome exists" { winapp ui wait-for "NavHome" -a $AppPid -t 3000 } |