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에서 보기