Loading...
Loading...
Automated 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 typing (send-keys), hover, drag-and-drop, touch and pen input, file pickers, flyouts, dialogs, persistence, accessibility audits, and screenshot/video capture. Works on any Windows app (Win32, WPF, WinForms, WinUI 3, packaged or unpackaged).
npx skill4agent add microsoft/win-dev-skills winui-ui-testingwinapp uiLostFocuswinapp ui <command>ui-tests.ps1winapp uistatuslist-windowsinspectsearchget-propertyget-valueget-focusedwait-forinvokeclickset-valuefocusscrollscroll-into-viewsend-keyshoverdragtouchpenscreenshotrecordwinapp ui --cli-schemawinapp ui <verb> --helpwinapp ui inspect -a <PID> --interactiveui-tests.ps1# ui-tests.ps1
param([Parameter(Mandatory)][int]$AppPid)
# NOTE: Do NOT name the parameter $Pid — it's read-only in PowerShell
$ErrorActionPreference = 'Continue'
$pass = 0; $fail = 0; $results = @()
# Get main window HWND (avoids PopupHost interference with JSON parsing)
$windows = winapp ui list-windows -a $AppPid --json 2>$null | ConvertFrom-Json
$hwnd = ($windows | Where-Object { $_.title -ne "PopupHost" } | Select-Object -First 1).hwnd
function Test-UI {
param([string]$Name, [scriptblock]$Script)
# IMPORTANT: Inside $Script, use 'throw' to signal failure — NOT 'exit 1'
# (exit terminates the entire script, not just the test)
try {
$output = & $Script 2>&1
if ($LASTEXITCODE -eq 0) {
$script:pass++; $script:results += @{ name = $Name; status = "PASS" }
} else {
$script:fail++; $script:results += @{ name = $Name; status = "FAIL"; detail = "$output" }
}
} catch {
$script:fail++; $script:results += @{ name = $Name; status = "FAIL"; detail = "$_" }
}
}
# ─── Element Existence ───
Test-UI "NavHome exists" { winapp ui wait-for "NavHome" -a $AppPid -t 3000 }
Test-UI "NavSettings exists" { winapp ui wait-for "NavSettings" -a $AppPid -t 3000 }
# ─── Navigation ───
Test-UI "Navigate to Settings" { winapp ui invoke "NavSettings" -a $AppPid }
Test-UI "Settings page loaded" { winapp ui wait-for "TxtUserName" -a $AppPid -t 3000 }
# ─── Interactions ───
Test-UI "Set username" { winapp ui set-value "TxtUserName" "TestUser" -a $AppPid }
Test-UI "Click Save" { winapp ui invoke "BtnSave" -a $AppPid } # commits the TextBox binding
Test-UI "Username value set" {
winapp ui wait-for "TxtUserName" -a $AppPid --value "TestUser" -t 2000
}
# ─── Value assertions for different control types ───
Test-UI "Theme is System default" {
winapp ui wait-for "CmbTheme" -a $AppPid --value "System default" -t 2000
}
Test-UI "Logging is off" {
winapp ui wait-for "TglLogging" -a $AppPid --value "Off" -t 2000
}
# ─── Accessibility Audit ───
# Only audit controls in the app's main window (exclude OS picker/popup controls)
$allElements = (winapp ui inspect -a $AppPid --interactive --json 2>$null | ConvertFrom-Json).elements
$appElements = @($allElements | Where-Object {
$_.type -match 'Button|TextBox|ComboBox|CheckBox|ToggleSwitch|TabItem|Edit' -and
$_.name -notmatch 'Minimize|Maximize|Close|System' -and # window chrome
$_.className -notmatch 'PickerHost|#32770|CabinetWClass' # OS dialogs
})
$missingId = @($appElements | Where-Object { -not $_.automationId })
if ($missingId.Count -eq 0) {
$pass++; $results += @{ name = "All app controls have AutomationId"; status = "PASS" }
} else {
$fail++
$names = ($missingId | ForEach-Object { "$($_.type) '$($_.name)'" }) -join ", "
$results += @{ name = "AutomationId coverage"; status = "FAIL"; detail = "Missing: $names" }
}
# ─── State Screenshots (capture each meaningful state for visual review) ───
New-Item -ItemType Directory -Force -Path "screenshots" | Out-Null
winapp ui screenshot -a $AppPid -o "screenshots/01-initial.png" 2>$null
# ...take more screenshots after key interactions above (mode switches, dialogs opened, etc.)
# ─── Final Screenshot ───
winapp ui screenshot -a $AppPid -o "test-screenshot.png" 2>$null
# ─── Results ───
Write-Host "`nPassed: $pass | Failed: $fail"
$results | Where-Object { $_.status -eq "FAIL" } | ForEach-Object {
Write-Host " FAIL: $($_.name) — $($_.detail)" -ForegroundColor Red
}
$results | ConvertTo-Json | Out-File "test-results.json"
if ($fail -gt 0) { exit 1 } else { exit 0 }| Requirement type | Test approach |
|---|---|
| "Has a button that does X" | |
| "Text field shows value" | |
| "Status bar contains text" | |
| "Dropdown is set to X" | |
| "Toggle is on/off" | |
| "Navigation between pages" | |
| "Open file dialog" | |
| "Save file dialog" | Same as open — find picker with |
| "Right-click context menu" | |
| "Keyboard shortcut (Ctrl+S, etc.)" | |
| "Type into a TextBox/RichEditBox" | |
| "Tooltip / hover flyout appears" | |
| "Drag to reorder / resize / slider" | |
| "Touch gesture (swipe/pinch/stretch)" | |
| "Capture a repro clip of a flow" | |
| "Confirmation dialog" | |
| "Data persists" | Set values, |
| "All controls accessible" | |
.\ui-tests.ps1 -AppPid <PID>test-results.jsonPASSwinapp ui screenshotno…winui-designtest-results.json.\BuildAndRun.ps1.\ui-tests.ps1 -AppPid <PID>launched (PID: XXXXX)wait-for --value| Control type | | Example |
|---|---|---|
| TextBlock / Label | Name property | |
| TextBox / NumberBox | ValuePattern | |
| RichEditBox | TextPattern | |
| ComboBox | Selected item (SelectionPattern) | |
| ToggleSwitch | Toggle state (On/Off) | |
| CheckBox | Toggle state (On/Off) | |
| Assertion | Command |
|---|---|
| Element exists | |
| Element has exact value | |
| Value contains text | |
| Element gone | |
| Specific property | |
| Button clickable | |
| Set then verify | |
| Screenshot | |
| Dialog appeared | |
| Right-click menu | |
| Read raw property | |
| Read current value (no wait) | |
| Scroll item into view | |
| Set keyboard focus | |
| Type real keystrokes into a control | |
| Fire a keyboard accelerator/shortcut | |
| Hover to show tooltip/flyout | |
| Drag / reorder / slider gesture | |
| Touch gesture | |
| Pen / ink stroke | |
| Record a video clip | |
PickerHost# 1. Trigger the picker
winapp ui invoke "BtnOpenFile" -a $AppPid
# 2. Find the picker window (it's a dialog owned by the app window)
Start-Sleep 1
$allWindows = winapp ui list-windows -a $AppPid --json 2>$null | ConvertFrom-Json
$picker = $allWindows | Where-Object { $_.title -match "Open|Save" }
$pickerHwnd = $picker.hwnd
# 3. Interact with the picker using -w <HWND>
# Type a filename:
winapp ui set-value "FileNameControlHost" "test.txt" -w $pickerHwnd
# Click Open/Save:
winapp ui invoke "Open" -w $pickerHwnd # or "Save", "Cancel"
# Or cancel:
winapp ui invoke "Cancel" -w $pickerHwnd
# 4. Verify the app processed the file
winapp ui wait-for "StatusBar" -a $AppPid -p Name --value "opened" -t 3000winapp ui inspect -w <pickerHwnd> --interactive# 1. Right-click to open a ContextFlyout
winapp ui click "LstItems" -a $AppPid --right
Start-Sleep 0.5
# 2. The flyout MenuItems appear in the tree immediately
# Find them with inspect or search:
winapp ui inspect -a $AppPid --interactive # shows MnuCopy, MnuDelete, etc.
# 3. Click a flyout item
winapp ui invoke "MnuCopy" -a $AppPid
# 4. Verify the action
winapp ui wait-for "StatusText" -a $AppPid -p Name --value "Copied" -t 2000# Click the menu header to open
winapp ui invoke "FileMenu" -a $AppPid
Start-Sleep 0.5
# Click the sub-item
winapp ui invoke "MenuSaveAs" -a $AppPid# 1. Trigger the dialog
winapp ui invoke "BtnDelete" -a $AppPid
Start-Sleep 0.5
# 2. The dialog buttons appear in the tree
# For a standard confirmation dialog:
winapp ui search "Primary" -a $AppPid --json # finds the primary button
winapp ui invoke "Primary" -a $AppPid # click "Yes"/"Delete"/"Save"
# Or:
winapp ui invoke "Secondary" -a $AppPid # click "No"/"Don't Save"
winapp ui invoke "Close" -a $AppPid # click "Cancel"
# 3. Wait for dialog to dismiss
winapp ui wait-for "Primary" -a $AppPid --gone -t 3000inspectinvokeclickset-value-a <PID>-w <HWND>send-keysentertabf5ctrl+shift+tvk=0x42--viapost-messageTextChangedKeyDownsend-inputKeyDownTextChangedKeyboardAcceleratorctrl+tTextBoxwinapp ui send-keys "ctrl+s" -a $AppPid --via send-input # fire a Ctrl+S accelerator
winapp ui send-keys "hello world" --target "TxtName" -a $AppPid --via send-input # focus then type
winapp ui send-keys --verbatim "down down enter" -a $AppPid # type the words, not the keys--targettext=<literal>--verbatimwin+ralt+f4--allow-system-keys--via send-inputwin+lhover--dwell-timewinapp ui hover "BtnInfo" -a $AppPid
winapp ui wait-for "InfoTooltip" -a $AppPid -t 2000drag<from><to>x,yinspect--hold-ms--dwell-mswinapp ui drag "ItemA" "ItemB" -a $AppPid # reorder ItemA onto ItemB
winapp ui drag "SldVolume" 300,120 -a $AppPid # drag a slider thumb to a pointtouch-gtapdouble-taplong-pressswipepinchstretch--direction--distance--to-point--fingerswinapp ui touch "LstFeed" -g swipe --direction up --distance 400 -a $AppPid
winapp ui touch "ImgPhoto" -g stretch --distance 200 -a $AppPid # pinch-to-zoompen--path "x,y x,y …"--pressure--tilt-x--tilt-y--eraserwinapp ui pen "InkCanvas" --path "50,50 120,80 200,60" --pressure 0.8 -a $AppPid
winapp ui pen "InkCanvas" --path "50,50 200,60" --eraser -a $AppPidwinapp ui record--duration-sec Nwinapp ui record -a $AppPid --duration-sec 6 --fps 30 -o "flow.mp4"--max-edge N--capture-screenscreenshotset-valuex:Bind TwoWayLostFocusset-valueUpdateSourceTrigger=PropertyChangedinvokeclickfocusset-valueLostFocusRichEditBoxsend-keysset-valueRichEditBoxRichTextBoxfocus--targetsend-keys "…" --via send-inputKeyDownKeyboardAcceleratorGet-Content $dataFile | ConvertFrom-Json$AppPid$Pid$Pid--value-p-p PropertyName --valueIsEnabled-w <HWND>-a PIDlist-windowsStart-Sleep