Skip to main content

winui-ui-testing

Write and run automated UI tests for Windows desktop apps (WinUI 3, WPF, WinForms, Win32) with the winapp ui harness: one batch script, assertions, input, file pickers, dialogs, screenshots, locally or in Windows Sandbox. Use when asked for UI tests or to verify a feature end to end.

来源信息

仓库
microsoft/winappCli
最近来源活动
2026年10月8日 23:31
检测到的 SKILL.md 语言
英语
星标
1,314
分支
88

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
winui-ui-testing
description
Write and run automated UI tests for Windows desktop apps (WinUI 3, WPF, WinForms, Win32) with the winapp ui harness: one batch script, assertions, input, file pickers, dialogs, screenshots, locally or in Windows Sandbox. Use when asked for UI tests or to verify a feature end to end.
### Scope and execution boundary `winapp ui` uses Windows UI Automation (UIA), so the AutomationId-based workflow works with any Windows desktop framework, packaged or unpackaged. Skip WinUI-specific binding/dialog advice for other frameworks. Windows Sandbox keeps synthetic input off the user's desktop; target selection is in Step 1. Discover the installed contract with `winapp run --help`, `winapp target --help`, and `winapp ui <verb> --help`. Use `--on sandbox`, not `--sandbox` or a top-level `sandbox` command. Do not enable Windows features or launch an app without the task's permission. - Prerequisites and enablement: see `winui-setup`. - Project builds/publishes execute on the **host**; deployment, app launch, and `ui --on sandbox` execute in the **guest**. Run the batch script below on the host, not inside `target exec` (which would double-route). - Real input and capture require an unlocked host and a connected, nonminimized Sandbox client. Tree inspection may work while input cannot; a readable tree is not an input-readiness check. - `winapp target snapshot sandbox --json` is a read-only readiness query: it neither starts nor reconnects a guest. Use it to diagnose readiness rather than probing the user's desktop. - The persistent guest is shared, not isolation between mutually untrusted workflows. Coordinate with other users; there is no supported `--unique-identity` option. Use separate machines for mutually untrusted work. - Guest `--debug-output` supports **packaged** apps only. ### Approach and command discovery Prefer a single scripted batch over a long series of interactive calls. If you wrote the app, use its XAML/source AutomationIds directly; otherwise inspect its current tree and read source for hidden/lazy flyouts and dialogs. Either way, validate a real window/tree before passing an accessibility audit. Core verbs: `list-windows`, `inspect`, `search`, `get-property`, `get-value`, `wait-for`; `invoke`, `click`, `set-value`, `focus`, `scroll-into-view`; `send-keys`, `hover`, `drag`, `touch`, `pen`; `screenshot`, `record`. Use each verb's `--help` for selectors and options, rather than guessing from this short reference. ### Step 1: Select a target, then keep the PID and target together Prefer `sandbox` when Windows Sandbox is available; otherwise tell the user and choose `local`. **If the user explicitly requested Windows Sandbox and it is unavailable, stop** and point them to the enablement steps in `winui-setup`. A stopped guest does not prove unavailability: `target snapshot` can report no running target while the feature is enabled. App build/test failures are not Sandbox unavailability. The same policy applies to diagnostics: unpackaged apps can't use guest `--debug-output`, so diagnose them locally unless Sandbox was explicitly requested. Pass the selected target to the template: it launches with `winapp run . --on sandbox --detach --json` for a guest, or omits `--on sandbox` for local execution. Reuse an already-running app only when its captured target matches the selected target and the guest has not been recreated. Never pass a guest PID to default-host `winapp ui`. If a target becomes unavailable after selection, report it and select again under the same policy; the script itself never retries in another target. The run JSON includes **`ProcessId`**, **`ProcessScope`**, and **`UiTargetArgs`** (PascalCase). In Windows Sandbox, `UiTargetArgs` is **`--on sandbox -a <guestPID>`**: preserve both parts, not just the number. For an explicitly requested **unpackaged** guest run, the launch result has no app PID: launch separately, find the intended app by process/title with target-wide `winapp ui list-windows --on sandbox --json`, then pass its PID with `-Target sandbox -AppPid <PID> -AppScope sandbox`. Never use the containment PID. A fresh guest invalidates old PIDs/HWNDs. ### Step 2: Write the host-side batch Create `ui-tests.ps1`, replace the sample AutomationIds/expected values with the app's requirements, and add the relevant examples below **inside the outer `try`**, before its `finally`. The two small command helpers only route and check CLI calls; they do not implement a target manager. Use them for **every** added command so an earlier native failure cannot be hidden by a later success. ```powershell # ui-tests.ps1 param( [Parameter(Mandatory)][ValidateSet('sandbox', 'local')][string]$Target, [int]$AppPid = 0, [ValidateSet('sandbox', 'local')][string]$AppScope, [ValidateNotNullOrEmpty()][string]$WorkflowId = [guid]::NewGuid().ToString('N'), [string]$ArtifactDirectory = (Join-Path '.\ui-test-artifacts' ([guid]::NewGuid().ToString('N'))) ) $ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $false $ScopeArgs = @(if ($Target -eq 'sandbox') { '--on'; 'sandbox' }) $previousWorkflowId = $env:WINAPP_UI_WORKFLOW_ID $env:WINAPP_UI_WORKFLOW_ID = $WorkflowId $results = [Collections.Generic.List[object]]::new() $screenshots = [Collections.Generic.List[string]]::new() $uiStarted = $false function Invoke-WinAppChecked { param([string[]]$Arguments) $global:LASTEXITCODE = $null $output = @(& winapp @Arguments) $code = $global:LASTEXITCODE if ($null -eq $code -or $code -ne 0) { throw "winapp $($Arguments -join ' ') failed (exit $code). $($output -join "`n")" } $output } function Invoke-Ui { param([string[]]$Arguments, [string]$Window) $selector = if ($Window) { @('-w', $Window) } else { @('-a', "$AppPid") } Invoke-WinAppChecked (@('ui') + $Arguments + $ScopeArgs + $selector) } function Test-UI { param([string]$Name, [scriptblock]$Script) try { & $Script | Out-Null $results.Add(@{ name = $Name; status = 'PASS' }) } catch { $results.Add(@{ name = $Name; status = 'FAIL'; detail = "$_" }) } } function Save-Screenshot { param([string]$Name) $path = Join-Path $ArtifactDirectory $Name if (Test-Path -LiteralPath $path) { throw "Choose a new evidence path: $path" } Invoke-Ui @('screenshot', '--output', $path) -Window $hwnd | Out-Null if (-not (Test-Path -LiteralPath $path -PathType Leaf) -or (Get-Item -LiteralPath $path).Length -eq 0) { throw "Screenshot was not delivered to the host: $path" } $screenshots.Add($path) } try { $ArtifactDirectory = [IO.Path]::GetFullPath($ArtifactDirectory) New-Item -ItemType Directory -Force -Path $ArtifactDirectory | Out-Null if ($AppPid -lt 0) { throw 'AppPid must be positive, or zero to launch.' } if ($AppPid -gt 0) { if ($AppScope -ne $Target) { throw 'Reusing a PID requires matching -AppScope and -Target from the same live target.' } } else { $launch = Invoke-WinAppChecked (@('run', '.') + $ScopeArgs + @('--detach', '--json')) | ConvertFrom-Json if ($launch.Error -or -not $launch.ProcessId -or [int]$launch.ProcessId -le 0) { throw "No app ProcessId. For unpackaged guest runs, discover the app in that target and rerun with -AppPid and -AppScope. $($launch.Error)" } $AppPid = [int]$launch.ProcessId if ($Target -eq 'sandbox' -and ($launch.ProcessScope -ne 'sandbox' -or $launch.UiTargetArgs -ne "--on sandbox -a $AppPid")) { throw 'Launch response is missing the expected Sandbox ProcessScope / UiTargetArgs pair.' } if ($Target -eq 'local' -and $launch.ProcessScope -and $launch.ProcessScope -ne 'local') { throw 'Launch response belongs to a different target.' } } $uiStarted = $true $windows = @(Invoke-Ui @('list-windows', '--json') | ConvertFrom-Json) $main = $windows | Where-Object { $_.hwnd -and $_.hwnd -ne '0' -and $_.title -ne 'PopupHost' } | Select-Object -First 1 if (-not $main) { throw 'No app window found in the selected target; rediscover after a fresh guest.' } $hwnd = [string]$main.hwnd Test-UI 'Initial screenshot delivered' { Save-Screenshot '01-initial.png' } Test-UI 'Home exists' { Invoke-Ui @('wait-for', 'NavHome', '-t', '3000') } Test-UI 'Navigate to Settings' { Invoke-Ui @('invoke', 'NavSettings') Invoke-Ui @('wait-for', 'TxtUserName', '-t', '3000') } Test-UI 'Save username' { Invoke-Ui @('set-value', 'TxtUserName', 'TestUser') Invoke-Ui @('invoke', 'BtnSave') Invoke-Ui @('wait-for', 'TxtUserName', '--value', 'TestUser', '-t', '2000') } Test-UI 'Default values' { Invoke-Ui @('wait-for', 'CmbTheme', '--value', 'System default', '-t', '2000') Invoke-Ui @('wait-for', 'TglLogging', '--value', 'Off', '-t', '2000') } Test-UI 'App controls have AutomationIds' { $inspection = Invoke-Ui @('inspect', '--interactive', '--json') -Window $hwnd | ConvertFrom-Json function Get-Elements($nodes) { foreach ($node in $nodes) { if ($null -ne $node) { $node; Get-Elements $node.children } } } $allElements = @(Get-Elements @($inspection.windows | ForEach-Object { $_.elements })) if (-not $allElements.Count) { throw 'Inspection returned no elements; not an accessibility PASS.' } $appElements = @($allElements | Where-Object { $_.type -match 'Button|TextBox|ComboBox|CheckBox|ToggleSwitch|TabItem|Edit' -and $_.name -notmatch 'Minimize|Maximize|Close|System' -and $_.className -notmatch 'PickerHost|#32770|CabinetWClass' }) if (-not $appElements.Count) { throw 'No app controls were audited; adjust selectors rather than passing.' } $missingId = @($appElements | Where-Object { -not $_.automationId }) if ($missingId.Count) { throw "Missing AutomationIds: $(($missingId | ForEach-Object { "$($_.type) '$($_.name)'" }) -join ', ')" } } Test-UI 'Final screenshot delivered' { Save-Screenshot '02-settings.png' } } catch { $results.Add(@{ name = 'Target setup / test execution'; status = 'FAIL'; detail = "$_" }) } finally { try { if ($uiStarted) { Invoke-WinAppChecked (@('ui', 'yield') + $ScopeArgs) | Out-Null } } catch { $results.Add(@{ name = 'Release UI workflow'; status = 'FAIL'; detail = "$_" }) } finally { $env:WINAPP_UI_WORKFLOW_ID = $previousWorkflowId } } $failed = @($results | Where-Object { $_.status -eq 'FAIL' }).Count $report = [ordered]@{ target = $Target; appPid = $AppPid; workflowId = $WorkflowId passed = $results.Count - $failed; failed = $failed results = @($results.ToArray()); screenshots = @($screenshots.ToArray()) } $report | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath '.\test-results.json' -Encoding utf8 Write-Host "Passed: $($report.passed) | Failed: $failed | Results: .\test-results.json" if ($failed) { exit 1 } exit 0 ``` Within a test, **throw**, not `exit`; the script fails on CLI errors, empty inspections, and undelivered evidence. Screenshots target the known main HWND so each lands at one exact host path; use `--capture-screen` when popups must be included. ### Step 3: Run, read, and visually verify ```powershell # Preferred when Windows Sandbox is available: .\ui-tests.ps1 -Target sandbox if ($LASTEXITCODE -ne 0) { throw 'UI batch failed; read test-results.json.' } $report = Get-Content -LiteralPath '.\test-results.json' -Raw | ConvertFrom-Json $report.screenshots ``` For a captured, still-live guest PID, use `.\ui-tests.ps1 -Target sandbox -AppPid 1234 -AppScope sandbox`. For a local run (see Step 1), use `.\ui-tests.ps1 -Target local`, optionally with a local PID and `-AppScope local`. Never reuse a guest PID locally or switch targets to make failing tests pass. View **each host PNG** listed in `test-results.json` with the image-viewing tool. UIA PASS cannot detect clipping, overlap, incorrect theming, or content bleeding past its container. Fail visual review for unintended scrollbars, unintended ellipses, clipped hero/right-edge controls, overlapping rows, unbalanced whitespace, cramped/vast spacing, incorrect Light/Dark/High Contrast, or missing focus/hover/error states. Capture meaningful states immediately after their interactions, not just at the end. If fixes are requested, batch-fix failures, rebuild/relaunch with the same target using the template (omit `-AppPid` to launch again), and capture the new PID. Maximum **two** fix-and-rerun cycles; then report remaining failures. Do not declare completion without the structured results **and** visual review. ### Assertions and coverage Write tests for every requested requirement. Use `wait-for --value` for TextBox/NumberBox values, RichEditBox text, ComboBox selection, toggle/checkbox `On`/`Off`, and TextBlock/label text. `--contains` matches a substring; `--gone` waits for disappearance. Use `-p IsEnabled --value True` only for a specific property. Invoke buttons/nav items, wait for page-specific elements, and test default/error/empty states. The following examples are additions to the template, not independent shells. `Invoke-Ui` always supplies the chosen scope plus `-a $AppPid`, or scope plus `-w $Window`. For raw reads, pass `--json` and parse it; plain stdout can include advisory messages. Use `scroll-into-view` before asserting a virtualized item below the fold. ```powershell Test-UI 'Status and enabled state' { Invoke-Ui @('wait-for', 'StatusBar', '--value', 'saved', '--contains', '-t', '3000')
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看