## Summary Adds end‑to‑end UI tests on the `Microsoft.PowerToys.UITest.Next` (winappcli) framework for three modules, grows the shared `.Next` test framework with the helpers those suites needed, and adds the CI plumbing that lets shell‑extension tests exercise the **real** Windows 11 modern context menu. Also ships two agent skills that document how to write and run these tests. Product runtime behavior is **unchanged** — the only product edits are test‑observability hooks in Peek and a unit‑test project exclude. Closes: https://github.com/microsoft/PowerToys/issues/40660 https://github.com/microsoft/PowerToys/issues/49424 https://github.com/microsoft/PowerToys/issues/40661 ## What's added ### New UI test suites - **Image Resizer** — `src/modules/imageresizer/tests/ImageResizer.UITests`: context‑menu enable/disable tracking, the resize dialog, custom presets, every fit mode, every unit, filename format, keep‑date, shrink‑only, replace‑in‑place, and orientation. - **Peek** — `src/modules/peek/Peek.UITests.Next`: file‑preview coverage across image/text/archive/ markdown types with per‑arch visual baselines. - **File Explorer add‑ons** — `src/modules/previewpane/PreviewPane.UITests`: Preview Pane handlers and thumbnail providers. ### `UITestAutomation.Next` framework - New helpers: `ExplorerShell` (Shell selection/view‑mode interop), `WaitHelper` (structured stable waits), `WindowControl` (foreground/context‑menu/process control), `VisualAssert` (image compare), `WindowHelper`. - Updates to `Session`, `UITestBase`, `SettingsConfigHelper`, `WinappCli`. - New `UITestAutomation.Next.UnitTests` project covering the new wait/settings/CLI helpers. ### CI — sign sparse MSIX so the modern menu registers - **`.pipelines/signSparsePackages.ps1`** — self‑signs each sparse context‑menu MSIX with a publisher‑matching test certificate and force‑trusts it (machine stores), so `AddPackageByUriAsync` succeeds on otherwise‑unsigned PR builds. Robust `signtool` discovery with a NuGet fallback; test‑only trust that asserts no security. - Wired into **`.pipelines/v2/templates/job-test-project.yml`** as a best‑effort step covering the run‑in‑place, machine‑install, and per‑user‑install locations. Signs nothing it can't (skips already‑signed packages) and never fails the job. ### Product changes (test observability only) - **Peek `FilePreview.xaml` / `.xaml.cs`** — a named `LoadingIndicator` and a hidden automation peer that exposes the current preview state as text, so tests can read load state deterministically. No runtime behavior change. - **`ImageResizer.UnitTests.csproj`** — exclude the sibling `ImageResizer.UITests\**` folder from the unit‑test compilation. ### Agent skills & docs - **New `ui-tests-local-vm` skill** — run `.Next` suites in persistent dockur/windows VMs: setup, agentic loop, image customization, troubleshooting, the shell‑extension **signing** reference, plus controller/guest scripts and VM templates. - **Updated `ui-tests-migration` skill** — WinAppDriver/Selenium → `.Next` porting guidance (CI stability, Explorer/shell‑extension test design, patterns & pitfalls). - **`doc/devdocs/development/ui-tests.md`** — updated for the `.Next` workflow. ## Testing - All three suites pass locally and in CI across **x64 Win10**, **x64 Win11**, and **arm64** (machine and per‑user install legs). ## Reviewer notes - No product runtime behavior changes; product edits are limited to the Peek test hooks above. - The CI signing step is a **test‑only** trust anchor (self‑signed, scoped to the agent) and is best‑effort, so it can only add modern‑menu coverage and never regress the job.
9.4 KiB
Agentic local-VM UI-test loop
This loop assumes the test project follows Microsoft.PowerToys.UITest.Next, builds as a
Microsoft.Testing.Platform executable, and has already passed the ui-tests-migration design and
CI-stability checks.
Run the full suite twice: on a Windows 10 Enterprise LTSC 2021 guest first, then the same unfiltered suite on a separate Windows 11 guest. Both must be green. Windows 11 is not a filtered follow-up - tests with no Windows 11 content still fail there for shell, compositor, and timing reasons.
1. Build on the host
Build only; do not launch PowerToys or tests on the host when the task forbids it.
tools\build\build.cmd `
-Path src\modules\<Module>\Tests\<Module>.UITests.Next `
-Platform x64 -Configuration Debug
git rev-parse HEAD
Exit code 0 is required. Record the build label before packaging.
2. Create a lean exchange
The exchange can be any host folder. Keeping it under the scaffold's shared folder keeps VM assets
together and out of the repository; the controller mirrors it into the guest at
C:\PowerToysUiTestExchange\<name>:
<VmRoot>\shared\PowerToysUiTests\<Module>\
|-- ui-tests.zip
|-- powertoys-runtime.zip
|-- winappcli.zip
|-- dotnet-runtime.zip
|-- product-overlay.zip # optional
|-- MicrosoftEdgeWebView2RuntimeInstallerX64.exe # optional
`-- LocalVmResults\
The controller writes each request and its durable evidence under LocalVmResults.
Package archive contents directly:
Compress-Archive -Path '<test-output>\*' `
-DestinationPath '<exchange>\ui-tests.zip' -Force
Compress-Archive -Path '<product-runtime>\*' `
-DestinationPath '<exchange>\powertoys-runtime.zip' -Force
Build the product runtime in Release for any shell-extension test. The runtime context-menu
registration for Image Resizer, File Locksmith, New+, and PowerRename is compiled behind
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG), so a Debug runtime silently omits it: the
module enables and logs normally, but the entry never appears in Explorer and menu assertions fail
with no obvious cause (for example, "Explorer did not show 'Resize with Image Resizer'"). CI ships
Release for this reason. If you must validate against a Debug runtime, rebuild only the affected
module DLL with ENABLE_REGISTRATION defined and overlay it via product-overlay.zip.
Use the repository-pinned winappcli build and a private .NET runtime matching the test executable. Even when .NET 10 is installed in the VM baseline, the private runtime remains the default for reproducibility and revision comparison.
The controller copies its bundled templates/run-ui-tests.ps1, computes per-component SHA-256
hashes, writes a run-specific request, and never maps the repository or build output directly into
Windows.
3. Validate the plan
Always run the first request with -PlanOnly:
pwsh .github\skills\ui-tests-local-vm\scripts\Invoke-LocalVmUiTest.ps1 `
-VmName PowerToysUiTest-Win10 `
-VmRoot X:\PowerToysUiTestVm `
-ExchangeRoot X:\PowerToysUiTestVm\shared\PowerToysUiTests\<Module> `
-TestExecutable <Module>.UITests.Next.exe `
-Filter 'Name=<focused-test>' `
-BuildLabel (git rev-parse HEAD) `
-PlanOnly
Check:
GuestExchangeRootin the plan is a path underC:\PowerToysUiTestExchange.- Test, product, winappcli, and .NET hashes are present.
- The filter uses
Name=,Name~,FullyQualifiedName~, orTestCategory=. - No password, token, or source path appears in the request.
4. Run one focused test
Use the default VM resource profile (4 vCPUs and 8 GB RAM) while creating and stabilizing tests. Do not begin on the constrained profile: first prove the test and product behavior with sufficient CPU and RAM.
pwsh .github\skills\ui-tests-local-vm\scripts\Invoke-LocalVmUiTest.ps1 `
-VmName PowerToysUiTest-Win10 `
-VmRoot X:\PowerToysUiTestVm `
-ExchangeRoot X:\PowerToysUiTestVm\shared\PowerToysUiTests\<Module> `
-TestExecutable <Module>.UITests.Next.exe `
-Filter 'Name=<focused-test>' `
-Platform x64Win10 `
-BuildLabel (git rev-parse HEAD) `
-DesktopWidth 1920 -DesktopHeight 1080 `
-SuiteTimeout 15m -TimeoutMinutes 25
Before the guest runner starts, the controller dispatches a probe into the interactive account and requires:
- User is the configured standard user and is not an administrator.
- Session ID is greater than zero.
- Explorer exists in that session.
- Display dimensions match the request, unless both are zero.
- The guest exchange folder is accessible.
The probe and test tasks execute under the provisioned PowerShell 7 pwsh.exe. The narrow
PowerShell Direct control channel remains inbox Windows PowerShell 5.1; no remoting endpoint or
firewall rule is enabled for PS7.
Failure here is BLOCKED, not a test failure.
Iterate on Windows 10 first; it is the faster loop. Point the same controller at the separate
Windows 11 VM and exchange with -Platform x64Win11 for the second pass. Do not reuse the Windows 10
guest disk as the Windows 11 guest.
5. Parse evidence
Each run writes:
LocalVmResults\localvm-<timestamp-guid>\
|-- controller-plan.json
|-- request.json
|-- desktop-probe.ps1
|-- desktop-probe.json
|-- progress.json
|-- status.json
|-- local-vm-ui-tests.log
`-- TestResults\
|-- <suite>.trx
`-- <logs, screenshots, recordings, attachments>
The controller prints scalar TRX counters and per-test outcomes. Read both status.json and TRX:
- Assertion-bearing TRX failures are
FAIL. - Skipped, inconclusive, or otherwise
NotExecutedtests areFAIL; requiretotal > 0andexecuted == totaleven when the test process exits 0. - Zero selected tests/MTP exit code 8 is
BLOCKED. - Missing desktop, control channel, archive, or status is
BLOCKED. - Proven display/profile/compositor differences are
ENVIRONMENT. - An N/M pass rate proves the execution loop ran, even when the task did not ask to stabilize tests.
Do not modify an already stabilized suite merely because the local VM differs from CI. Report the pass rate and group failures by controlling boundary first.
6. Iterate incrementally
After changing tests or product code:
- Build the touched project to exit code 0.
- Replace only the corresponding archive.
- Rerun the same focused filter with
-ReuseStagedPayload. - Confirm
RefreshedComponentscontains only the changed component. - Widen only after the focused behavior is understood.
pwsh .github\skills\ui-tests-local-vm\scripts\Invoke-LocalVmUiTest.ps1 `
<same parameters> `
-ReuseStagedPayload
The guest manifest persists under C:\PowerToysUiTestRun. Unchanged tests/product/winappcli/.NET
are not extracted again. WebView2 and other baseline tools remain installed.
The VM stays running after each run. Use -SkipStart when it is already healthy, and
-StopVmAfterRun only when no further iteration is expected.
7. Widen to the suite, on both guests
Use a bounded category filter, first on the Windows 10 guest:
-Filter 'TestCategory=<Module>' -SuiteTimeout 45m -TimeoutMinutes 60
Report:
- Executed, passed, failed, and error counts.
- Exact pass rate.
- Root-cause groups, not only test names.
- Guest user/session/display and payload fingerprint.
- Export errors independently from assertion failures.
Then run the same filter against the Windows 11 guest with -Platform x64Win11 and report that
evidence separately. Narrowing the Windows 11 run to Windows 11-specific tests does not satisfy this
step. The module is done only when both suites are fully green; a Windows 10 pass with an unrun or
red Windows 11 suite is an incomplete result, not a success.
Once the complete target suite is green, stop the guest, then let the controller restart it with the
Constrained profile (1 vCPU and 4 GB RAM). Pass the guest's config explicitly when one VM root owns
multiple guests:
pwsh <VmRoot>\Stop-LocalVm.ps1 -ConfigPath <VmRoot>\vm.config.win10.psd1
pwsh .github\skills\ui-tests-local-vm\scripts\Invoke-LocalVmUiTest.ps1 `
-VmName PowerToysUiTest-Win10 `
-ConfigurationPath <VmRoot>\vm.config.win10.psd1 `
-ResourceProfile Constrained `
-VmRoot <VmRoot> -ExchangeRoot <exchange> `
-TestExecutable <Module>.UITests.exe `
-Filter 'TestCategory=<Module>' -Platform x64Win10 `
-ReuseStagedPayload
Invoke-LocalVmUiTest.ps1 rejects a config whose VmName does not match, records the resource
profile in the request/result, and passes it to Start-LocalVm.ps1. Resource changes only apply
while the VM is off; stopping first is therefore required. Repeat separately on Windows 11. Keep the
default-profile TRX as the correctness baseline and classify failures that appear only under
constrained resources separately.
8. Confirm clean-profile behavior
A retained VM accumulates registry state, caches, thumbnail databases, WebView profiles, Settings, and first-run suppressions. Choose one final confirmation based on risk:
- Restore the baseline checkpoint with
Reset-LocalVm.ps1 -Restore. A standard checkpoint includes memory, so this returns to the captured logged-on desktop in seconds. - Rebuild the guest from media with
New-UiTestVm.ps1 -Forcewhen the checkpoint itself is suspect.
Do not call a retained run clean merely because the product archive was refreshed.
Revision comparison
Hold the guest, Windows build, display, account, tools, filter, and timeouts constant. Change only the intentional test/product archive and record both fingerprints. For a clean-baseline comparison, restore the same checkpoint before each revision.