PowerToys UI-Tests Migration (legacy → .Next)
Convert a PowerToys module's UI tests from the legacy WinAppDriver / Selenium / Appium harness
(Microsoft.PowerToys.UITest, in src/common/UITestAutomation/) to the new winappcli harness
(Microsoft.PowerToys.UITest.Next, in src/common/UITestAutomation.Next/).
The new harness shells out to winapp.exe and parses its JSON — no WinAppDriver server on :4723,
no Selenium/Appium NuGet packages, no WindowsElement/WindowsDriver. The public shape
(UITestBase, Session, Find<T>, By, element wrappers like ToggleSwitch) is deliberately
similar, so most of the work is mechanical API mapping plus reworking a few patterns that don't
translate one-to-one (XPath selectors, stateful elements, instance mouse/keyboard helpers).
When to use this skill
Use this skill when the task is to:
- Port a module's existing legacy UI tests to
.Next (e.g. "migrate the ScreenRuler UI tests to
the new framework", "convert FancyZones.UITests to winappcli").
- Create a new
[Module].UITests.Next project that re-implements the legacy tests with the new
harness, leaving the old project in place.
- Stand up brand-new
.Next UI tests for a module that has no UI tests at all, by reading the
module's human test sign-off markdown (e.g. ColorPickerUITest.md) and turning each manual
checklist item into an automated test.
- Validate a new or migrated suite in a local Windows VM through an unattended
build/package/deploy/run/TRX/diagnose loop. Use a retained VM for fast iteration and a restored
baseline checkpoint when clean-profile behavior matters.
This skill is the how: the framework differences, the API mapping, the project scaffolding, the
naming rules, the recurring PowerToys test recipes, and the build/validate loop. The what (which
module, which tests) comes from the calling prompt.
Reference implementation — read these working examples before porting anything. They are
the ground truth for "what good looks like" with each harness:
- New (
.Next): ColorPickerEndToEndTests.cs
— full end-to-end scenario (navigate Settings → toggle module → read shortcut → fire hotkey →
read overlay → click-capture → inspect editor), driven entirely through winappcli.
- Legacy: TestSpacing.cs
- TestHelper.cs
— a
UITestBase subclass plus a static helper that navigates, toggles, reads the shortcut, fires
the hotkey, and validates the clipboard.
- Worked Scenario-A port (validated 5/5, where the legacy suite scored 0/5 locally): the
ScreenRuler suite ported from the legacy project above lives in
ScreenRuler.UITests.Next/TestHelper.cs
- 5 test classes. It is the canonical port reference — cross-window toolbar discovery via
Session.FromProcess, a DPI-aware app.manifest, cursor centering, and patient hotkey
activation are all there because real runs needed them (see
references/patterns-and-pitfalls.md).
- Stateful/visual reference (validated 15/15 across Win10 x64, Win11 x64, and ARM64):
PeekFilePreviewTests.cs
demonstrates stable Explorer Shell selection, toggle-hotkey activation, process-preserving
pinning tests, renderer readiness, and composed WinUI/WebView visual baselines.
- Explorer/Shell-extension reference (validated across x64 and ARM64 CI):
FileExplorerAddonsTests.cs
demonstrates class-scoped runner reuse, one-time Shell restart, state-aware Preview pane
activation, exact Shell selection, deterministic icon sizes, provider-log readiness, and
failure media captured before Explorer teardown. Read
references/explorer-shell-tests.md before testing Explorer.
Required reads (in order)
- This
SKILL.md — the decision tree (which scenario), the naming rules, the high-level
workflow, and the build/validate loop.
- references/framework-differences.md — the conceptual
deltas you MUST internalize before writing code: winappcli engine, stateless elements, selector
grammar (no XPath/CssSelector), session scopes (window vs process), lifecycle/hygiene/module
pre-enablement, multi-window discovery, and what the new harness does NOT (yet) provide.
- references/api-mapping.md — the line-by-line cheat sheet:
namespaces,
By, Element actions/properties, Session, UITestBase, the static
Keyboard/Mouse/Clipboard helpers, and the element-wrapper catalog. Keep this open while editing.
- references/project-setup.md — csproj scaffold, naming/placement
rules,
.slnx registration, and how to build & run a .Next project. Uses the
templates/ starter files.
- references/porting-workflow.md — the two end-to-end
playbooks: A) port existing legacy tests, and B) author tests from a human sign-off
markdown when none exist.
- references/patterns-and-pitfalls.md — adaptable recipes
for the recurring PowerToys patterns (toggle a module + verify its process, read the activation
shortcut from a
ShortcutControl, fire a global hotkey reliably, inspect the clipboard, discover
overlay/editor windows) and the gotchas that bite during migration.
- references/explorer-shell-tests.md — required for tests
involving Explorer, preview handlers, thumbnail providers, Shell selection, view modes, or Shell
restarts. Covers lifecycle boundaries, authoritative signals, and failure evidence.
- references/ci-stability.md — the CI-stability capstone: the
Win32-window vs UIA-element mental model, state-boundary worksheet, stable-sample waits, retry
semantics, foreground/integrity constraints, process lifecycle, composed visual capture, and a
pre-flight checklist to apply BEFORE the first CI push. It also covers Release Runner/Settings
IPC authentication, the existing CI companion-signing mechanism, and why a visible Settings toggle
must never be rescued with a settings-file/restart fallback. Read this to spend one CI iteration
instead of six.
- ui-tests-local-vm — the live desktop execution loop:
scaffold or reuse a persistent Hyper-V VM, run as a true standard user, refresh only
changed payloads, iterate through durable TRX/evidence, and restore or recreate the baseline for
clean-profile validation.
Pick your scenario
flowchart TD
A[Module to migrate] --> B{Does a legacy<br/>UITests project exist?}
B -- Yes --> C["Scenario A: PORT<br/>Create [Module].UITests.Next<br/>Re-implement each legacy test"]
B -- No --> D{Is there a human test<br/>sign-off .md?}
D -- Yes --> E["Scenario B: GREENFIELD<br/>Create [Module].UITests<br/>Turn each checklist item into a test"]
D -- No --> F[Ask the user for the<br/>test spec / sign-off doc]
| Scenario |
Trigger |
New project name |
Source of test cases |
| A — Port |
A legacy [Module].UITests (or similar) project already exists and references UITestAutomation.csproj |
[Module].UITests.Next — keep the .Next suffix so it lives alongside the legacy project |
The existing legacy test methods (1:1 re-implementation) |
| B — Greenfield |
The module has no UI tests at all |
[Module].UITests — drop the .Next suffix; there's nothing to live alongside |
The module's human sign-off markdown (manual checklist), e.g. ColorPickerUITest.md |
Place the new project under src/modules/[Module]/Tests/[Module].UITests.Next/ (or
…/Tests/[Module].UITests/ for Scenario B). If the module already keeps tests in a different
Tests/ layout, match the module's existing convention rather than forcing this one — see
references/project-setup.md.
Keep it abstract. Every PowerToys module is unique and the legacy tests were written by
different people in different styles. Treat the recipes in this skill as adaptable patterns, not
a rigid script. Re-create the intent and assertions of each test; do not mechanically translate
brittle, harness-specific scaffolding (Selenium Actions, XPath walks, manual driver attaches) when
the new harness has a cleaner idiom.
High-level workflow
Create a TODO list and work top-to-bottom. Each step links to the reference that drives it.
- [ ] 1. Identify the module + scenario (A port / B greenfield) — this SKILL.md "Pick your scenario"
- [ ] 1a. Read the module's developer docs — `doc/devdocs/modules/<module>.md` (if the exact file is
missing, search `doc/devdocs/`, including `doc/devdocs/common/`) — to learn its
development-cycle specifics BEFORE writing tests: how its shell extensions / context menus
register, whether they need a **Release** build (`NDEBUG`) or a **signed** sparse MSIX package,
and any Explorer-restart or first-run needs. Skipping this produces opaque failures — e.g. a
context-menu entry never appears because a Debug build compiles registration out, or an
unsigned `.msix` fails to register (`0x800B0100`).
- [ ] 2. Read the two reference examples (ColorPicker .Next + ScreenRuler legacy) end-to-end
- [ ] 3. Inventory the source:
• Scenario A → list every [TestMethod] + shared helper in the legacy project
• Scenario B → read the module's sign-off .md; list each manual checklist item
• For each workflow → list every external boundary (runner, Explorer, HWND, renderer,
compositor, child process) and its authoritative ready signal
— references/porting-workflow.md
- [ ] 4. Internalize the deltas — references/framework-differences.md
- [ ] 5. Scaffold the new project (csproj + PerMonitorV2 app.manifest from templates, name per the
table, register in .slnx)
— references/project-setup.md
- [ ] 6. Re-implement tests, mapping each API as you go — references/api-mapping.md
+ recipes from references/patterns-and-pitfalls.md
- [ ] 6a. If Explorer/Shell is involved, apply references/explorer-shell-tests.md
- [ ] 7. Apply the CI-stability checklist BEFORE building — references/ci-stability.md
(stable authoritative signals, retry classification, foreground/integrity, lifecycle reset
scope, non-activating helper processes, composed capture, DPI manifest, single-module enable,
first-run suppression)
- [ ] 7a. If a test changes a module's enabled state through Settings, keep the real Settings UI +
immediate runtime assertion. Verify the selected UITest project is covered by the existing
`$requiresAuthenticatedSettingsIpc` companion-signing path in
`.pipelines/v2/templates/job-test-project.yml`; never add a test-side settings/restart fallback
for Release CI — [references/ci-stability.md](references/ci-stability.md#principle-5a--keep-module-lifecycle-tests-on-real-release-settings-ipc)
- [ ] 8. Build the new project to exit code 0 — this SKILL.md "Build & validate"
- [ ] 9. Run one deterministic test in the local VM and diagnose the first failure
— ../ui-tests-local-vm/SKILL.md
- [ ] 10. Rerun the focused test after each fix, then widen to the complete module suite with bounded
timeouts; parse TRX and verify durable evidence export
- [ ] 11. If the local VM is unavailable or unsupported, run on another live desktop or report the exact
environmental blocker; do not silently stop at compile validation
Build & validate
The .Next harness needs winapp.exe only at run time, not build time — the project has zero
managed dependency on the engine. So you can always compile-verify a migration even on an agent with
no winappcli installed.
# 0. FIRST build of a brand-new project: restore so the assets file exists, otherwise the build
# fails with NETSDK1004 "Assets file ... project.assets.json not found".
dotnet restore src\modules\<Module>\Tests\<Module>.UITests.Next\<Module>.UITests.Next.csproj -p:Platform=x64
# (Equivalently, run tools\build\build-essentials.cmd once at the start of the session.)
# 1. Build just the new test project (fast inner loop). Prefer the repo build script.
tools\build\build.cmd -Path src\modules\<Module>\Tests\<Module>.UITests.Next -Platform x64 -Configuration Debug
# Exit code 0 = success; non-zero = failure. On failure read the errors log next to the project:
# build.<Configuration>.<Platform>.errors.log
# Do not substitute `dotnet build` when UITestAutomation.Next's COM references are in the graph:
# .NET SDK MSBuild cannot run ResolveComReference and fails with MSB4803. Use the repo script or
# Visual Studio's full-framework MSBuild.exe; use `dotnet restore` only to create project.assets.json.
# 2. Run (needs a live desktop). A .Next project is a Microsoft.Testing.Platform Exe — run the
# produced exe directly with a TRX report; filter to one test/category for a tight loop.
$exe = "<repo>\x64\Debug\tests\<Module>.UITests.Next\net10.0-windows10.0.26100.0\<Module>.UITests.Next.exe"
& $exe --filter "TestCategory=<Cat>" --report-trx --report-trx-filename run.trx --results-directory <dir>
# --filter accepts "TestCategory=X" or "FullyQualifiedName~Y"; omit it to run everything.
# Exit 0 = all passed. Parse the .trx for per-test outcomes + failure messages.
- Default to persistent local VM validation —
ui-tests-local-vm. It keeps the interactive desktop and staged
tools, refreshes only changed archives, and returns durable status/TRX/evidence. Do not
modify stabilized tests merely to improve a VM-specific pass rate when the task only asks whether
the execution loop works. Finish clean-profile claims from a restored known baseline or a fresh
named VM volume.
- Design for CI stability up-front — references/ci-stability.md.
Before the first push, walk its pre-flight checklist (authoritative-signal retries instead of fixed
sleeps, navigation via UIA invoke, interaction-scoped foreground checks, non-activating helpers,
Win32 window/overlay detection, screen-capture cold-start handling, DPI manifest, single-module
enable, first-run suppression). Most "passes local, fails CI" loops come from skipping one of
these; applying them proactively is how you spend one CI iteration instead of six.
- Run it in a loop: write → build → run → diagnose → repeat. UI tests surface environment-real
failures (DPI scaling, cursor position, hotkey-arming races) that only a live run reveals. Start
with one deterministic test (e.g. the activation/toggle test), get it green, then widen.
- Diagnose from the artifacts, not from the assertion message. Every failed test attaches a
desktop screenshot, and in pipeline mode an MP4 of the run. Open them before forming any theory
— especially before concluding the product is broken. An assertion can only say "found 0 rows"; the
screenshot says whether the list was empty or whether your selector was wrong. This is the single
highest-leverage habit in the agentic loop: skipping it cost ~8 iterations and a confident but
entirely wrong product-defect report on File Locksmith (see
references/patterns-and-pitfalls.md Pitfall 26). If there is
no video, find out why rather than proceeding blind — the harness now prints the reason (a clean
Windows image without the Visual C++ redistributable cannot load the native encoder).
- First, run the legacy suite once for a baseline — and run it ELEVATED. The legacy harness
launches PowerToys via
ProcessStartInfo { Verb = "runas" } (elevated), so a non-elevated test
host can't complete the launch and every test fails at startup with a misleading Win32Exception
cascade — a false 0/N that looks like "the tests are broken" but is purely the run method. (That's
why VS Test Explorer passes them: VS runs as admin.) Run from an elevated terminal: start
WinAppDriver.exe on 127.0.0.1:4723, then run the built DLL with vstest.console.exe (see
references/porting-workflow.md §A0 for the -Verb RunAs recipe).
A measurement failure on a scaled (non-100%) display is usually a pre-existing DPI issue (Pitfall
12), not something the port must reproduce — the ScreenRuler legacy suite scores 4/5 elevated
here (Bounds fails at 150% scale) while the .Next port scores 5/5. .Next tests themselves
need no elevation (the new harness launches the runner non-elevated).
- Always build to exit code 0 before declaring the migration done. Fix every compile error — do
not leave
// TODO: port this stubs that break the build.
- Running the tests requires a live interactive desktop plus
winapp.exe
(winget install Microsoft.winappcli, or set WINAPP_CLI_PATH). The whole PowerToys runner is
launched by the harness (PowerToys.exe --open-settings) — you should see the Settings window
appear. If the environment has no desktop (headless agent), state that the project builds clean
and is ready to run, and list which source tests/checklist items each new [TestMethod] covers.
- New
.csproj files under src/ MUST <Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
right after <Project Sdk=...> (CI audits this). The template already does.
What NOT to do
- Do NOT delete or edit the legacy
[Module].UITests project in Scenario A. The .Next project
lives alongside it; removing the old one is a separate, explicit decision for the maintainers.
- Do NOT change product behaviour. This is a test-only migration. The one sanctioned product edit
is adding
AutomationProperties.AutomationId to a control that is otherwise unaddressable — an
icon-only button whose label lives in a tooltip has no UIA Name at all, and the alternative is a
brittle coordinate click. Use AutomationProperties.AutomationId, never x:Name (which also emits
a code-behind field); see references/patterns-and-pitfalls.md
Recipe 16. Anything larger — a hidden automation-peer TextBlock, a new property, a state string —
must be flagged for the user instead. (ColorPicker's ColorHexAutomationPeer hook is a documented,
pre-existing exception — see its class remarks.)
- Do NOT port the legacy plumbing literally. No Selenium
Actions, no WindowsDriver/WindowsElement,
no By.XPath/By.CssSelector, no :4723. Map them to the winappcli idioms in
references/api-mapping.md.
- Do NOT add a
ProjectReference to UITestAutomation.csproj (the legacy harness) — reference
UITestAutomation.Next.csproj only.
- Do NOT invent assertions for a vague sign-off item. If a checklist line has no observable
pass/fail signal, implement what you can and leave a clearly-marked
TestContext.WriteLine note
(or skip with an explanation) rather than asserting on something you can't actually read.
- Do NOT introduce new third-party NuGet dependencies. The
.Next harness is intentionally
dependency-free (MSTest only). Use the Win32-based helpers it already ships.
- Do NOT retry a toggle hotkey blindly. Once any target window appears, wait for initialization;
resending the chord may close a healthy window. Restart only after a terminal readiness failure.
- Do NOT replace or weaken visual baselines before proving capture is correct. Foreground HWND,
DWM z-order, composed WebView content, theme, and platform are separate failure sources.
- Do NOT work around a Release Runner
not-microsoft-signed Settings rejection in test code. Do
not seed the global enabled map, restart PowerToys, bypass authentication, or weaken the lifecycle
assertion. Reuse the pipeline's existing Runner/Settings companion-signing mechanism; see
references/ci-stability.md.
What is NICE to do
- Improve the framework when a helper is demonstrably reusable. Prefer small composable APIs
(
WaitHelper, WindowControl, ExplorerShell) over module-specific mega-helpers. Keep product
semantics such as Peek pin-state preservation in the module test.
1---2name: ui-tests-migration3description: Migrate and stabilize PowerToys UI tests from WinAppDriver/Selenium to Microsoft.PowerToys.UITest.Next and winappcli. Use for ports, new UITest projects, flaky CI tests, persistent local-VM validation on Hyper-V, resettable clean-baseline runs, Settings IPC authentication/test signing, Explorer/Shell selection, preview handlers, thumbnail providers, hotkey activation, stateful process lifecycle, composed WinUI/WebView visual baselines, or cross-window/foreground failures. Covers APIs, scaffolding, test design, diagnostics, agentic execution, and CI hardening. Keywords: UI test, UITests, UITestAutomation.Next, winappcli, WinAppDriver, Selenium, Settings IPC, not-microsoft-signed, Authenticode, local VM, Hyper-V, checkpoint, migrate, flaky, CI stability, Explorer, Shell extension, WebView2.4license: Complete terms in LICENSE.txt5---6
7# PowerToys UI-Tests Migration (legacy → `.Next`)
8
9Convert a PowerToys module's UI tests from the legacy **WinAppDriver / Selenium / Appium** harness
10(`Microsoft.PowerToys.UITest`, in `src/common/UITestAutomation/`) to the new **winappcli** harness
11(`Microsoft.PowerToys.UITest.Next`, in `src/common/UITestAutomation.Next/`).
12
13The new harness shells out to `winapp.exe` and parses its JSON — **no WinAppDriver server on :4723,
14no Selenium/Appium NuGet packages, no `WindowsElement`/`WindowsDriver`.** The public *shape*
15(`UITestBase`, `Session`, `Find<T>`, `By`, element wrappers like `ToggleSwitch`) is deliberately
16similar, so most of the work is mechanical API mapping plus reworking a few patterns that don't
17translate one-to-one (XPath selectors, stateful elements, instance mouse/keyboard helpers).
18
19## When to use this skill
20
21Use this skill when the task is to:
22
23- **Port** a module's existing legacy UI tests to `.Next` (e.g. "migrate the ScreenRuler UI tests to
24 the new framework", "convert FancyZones.UITests to winappcli").
25- **Create a new** `[Module].UITests.Next` project that re-implements the legacy tests with the new
26 harness, leaving the old project in place.
27- **Stand up brand-new** `.Next` UI tests for a module that has **no** UI tests at all, by reading the
28 module's human test **sign-off markdown** (e.g. `ColorPickerUITest.md`) and turning each manual
29 checklist item into an automated test.
30- **Validate a new or migrated suite in a local Windows VM** through an unattended
31 build/package/deploy/run/TRX/diagnose loop. Use a retained VM for fast iteration and a restored
32 baseline checkpoint when clean-profile behavior matters.
33
34This skill is the *how*: the framework differences, the API mapping, the project scaffolding, the
35naming rules, the recurring PowerToys test recipes, and the build/validate loop. The *what* (which
36module, which tests) comes from the calling prompt.
37
38> **Reference implementation — read these working examples before porting anything.** They are
39> the ground truth for "what good looks like" with each harness:
40> - **New (`.Next`)**: [ColorPickerEndToEndTests.cs](../../../src/modules/colorPicker/ColorPicker.UITests/ColorPickerEndToEndTests.cs)
41> — full end-to-end scenario (navigate Settings → toggle module → read shortcut → fire hotkey →
42> read overlay → click-capture → inspect editor), driven entirely through `winappcli`.
43> - **Legacy**: [TestSpacing.cs](../../../src/modules/MeasureTool/Tests/ScreenRuler.UITests/TestSpacing.cs)
44> + [TestHelper.cs](../../../src/modules/MeasureTool/Tests/ScreenRuler.UITests/TestHelper.cs)
45> — a `UITestBase` subclass plus a static helper that navigates, toggles, reads the shortcut, fires
46> the hotkey, and validates the clipboard.
47> - **Worked Scenario-A port (validated 5/5, where the legacy suite scored 0/5 locally)**: the
48> ScreenRuler suite ported from the legacy project above lives in
49> [ScreenRuler.UITests.Next/TestHelper.cs](../../../src/modules/MeasureTool/Tests/ScreenRuler.UITests.Next/TestHelper.cs)
50> + 5 test classes. It is the canonical port reference — cross-window toolbar discovery via
51> `Session.FromProcess`, a DPI-aware `app.manifest`, cursor centering, and patient hotkey
52> activation are all there because real runs needed them (see
53> [references/patterns-and-pitfalls.md](references/patterns-and-pitfalls.md)).
54> - **Stateful/visual reference (validated 15/15 across Win10 x64, Win11 x64, and ARM64)**:
55> [PeekFilePreviewTests.cs](../../../src/modules/peek/Peek.UITests.Next/PeekFilePreviewTests.cs)
56> demonstrates stable Explorer Shell selection, toggle-hotkey activation, process-preserving
57> pinning tests, renderer readiness, and composed WinUI/WebView visual baselines.
58> - **Explorer/Shell-extension reference (validated across x64 and ARM64 CI)**:
59> [FileExplorerAddonsTests.cs](../../../src/modules/previewpane/PreviewPane.UITests/FileExplorerAddonsTests.cs)
60> demonstrates class-scoped runner reuse, one-time Shell restart, state-aware Preview pane
61> activation, exact Shell selection, deterministic icon sizes, provider-log readiness, and
62> failure media captured before Explorer teardown. Read
63> [references/explorer-shell-tests.md](references/explorer-shell-tests.md) before testing Explorer.
64
65## Required reads (in order)
66
671. **This `SKILL.md`** — the decision tree (which scenario), the naming rules, the high-level
68 workflow, and the build/validate loop.
692. **[references/framework-differences.md](references/framework-differences.md)** — the conceptual
70 deltas you MUST internalize before writing code: winappcli engine, stateless elements, selector
71 grammar (no XPath/CssSelector), session scopes (window vs process), lifecycle/hygiene/module
72 pre-enablement, multi-window discovery, and what the new harness does NOT (yet) provide.
733. **[references/api-mapping.md](references/api-mapping.md)** — the line-by-line cheat sheet:
74 namespaces, `By`, `Element` actions/properties, `Session`, `UITestBase`, the static
75 Keyboard/Mouse/Clipboard helpers, and the element-wrapper catalog. Keep this open while editing.
764. **[references/project-setup.md](references/project-setup.md)** — csproj scaffold, naming/placement
77 rules, `.slnx` registration, and how to build & run a `.Next` project. Uses the
78 [templates/](templates/) starter files.
795. **[references/porting-workflow.md](references/porting-workflow.md)** — the two end-to-end
80 playbooks: **A)** port existing legacy tests, and **B)** author tests from a human sign-off
81 markdown when none exist.
826. **[references/patterns-and-pitfalls.md](references/patterns-and-pitfalls.md)** — adaptable recipes
83 for the recurring PowerToys patterns (toggle a module + verify its process, read the activation
84 shortcut from a `ShortcutControl`, fire a global hotkey reliably, inspect the clipboard, discover
85 overlay/editor windows) and the gotchas that bite during migration.
867. **[references/explorer-shell-tests.md](references/explorer-shell-tests.md)** — required for tests
87 involving Explorer, preview handlers, thumbnail providers, Shell selection, view modes, or Shell
88 restarts. Covers lifecycle boundaries, authoritative signals, and failure evidence.
898. **[references/ci-stability.md](references/ci-stability.md)** — the CI-stability capstone: the
90 Win32-window vs UIA-element mental model, state-boundary worksheet, stable-sample waits, retry
91 semantics, foreground/integrity constraints, process lifecycle, composed visual capture, and a
92 **pre-flight checklist** to apply BEFORE the first CI push. It also covers Release Runner/Settings
93 IPC authentication, the existing CI companion-signing mechanism, and why a visible Settings toggle
94 must never be rescued with a settings-file/restart fallback. Read this to spend one CI iteration
95 instead of six.
969. **[ui-tests-local-vm](../ui-tests-local-vm/SKILL.md)** — the live desktop execution loop:
97 scaffold or reuse a persistent Hyper-V VM, run as a true standard user, refresh only
98 changed payloads, iterate through durable TRX/evidence, and restore or recreate the baseline for
99 clean-profile validation.
100
101## Pick your scenario
102
103```mermaid
104flowchart TD
105 A[Module to migrate] --> B{Does a legacy<br/>UITests project exist?}
106 B -- Yes --> C["Scenario A: PORT<br/>Create [Module].UITests.Next<br/>Re-implement each legacy test"]
107 B -- No --> D{Is there a human test<br/>sign-off .md?}
108 D -- Yes --> E["Scenario B: GREENFIELD<br/>Create [Module].UITests<br/>Turn each checklist item into a test"]
109 D -- No --> F[Ask the user for the<br/>test spec / sign-off doc]
110```
111
112| Scenario | Trigger | New project name | Source of test cases |
113|---|---|---|---|
114| **A — Port** | A legacy `[Module].UITests` (or similar) project already exists and references `UITestAutomation.csproj` | **`[Module].UITests.Next`** — keep the `.Next` suffix so it lives **alongside** the legacy project | The existing legacy test methods (1:1 re-implementation) |
115| **B — Greenfield** | The module has **no** UI tests at all | **`[Module].UITests`** — **drop** the `.Next` suffix; there's nothing to live alongside | The module's human sign-off markdown (manual checklist), e.g. `ColorPickerUITest.md` |
116
117Place the new project under **`src/modules/[Module]/Tests/[Module].UITests.Next/`** (or
118`…/Tests/[Module].UITests/` for Scenario B). If the module already keeps tests in a different
119`Tests/` layout, match the module's existing convention rather than forcing this one — see
120[references/project-setup.md](references/project-setup.md).
121
122> **Keep it abstract.** Every PowerToys module is unique and the legacy tests were written by
123> different people in different styles. Treat the recipes in this skill as *adaptable patterns*, not
124> a rigid script. Re-create the **intent and assertions** of each test; do not mechanically translate
125> brittle, harness-specific scaffolding (Selenium `Actions`, XPath walks, manual driver attaches) when
126> the new harness has a cleaner idiom.
127
128## High-level workflow
129
130Create a TODO list and work top-to-bottom. Each step links to the reference that drives it.
131
132```markdown
133- [ ] 1. Identify the module + scenario (A port / B greenfield) — this SKILL.md "Pick your scenario"
134- [ ] 1a. Read the module's developer docs — `doc/devdocs/modules/<module>.md` (if the exact file is
135 missing, search `doc/devdocs/`, including `doc/devdocs/common/`) — to learn its
136 development-cycle specifics BEFORE writing tests: how its shell extensions / context menus
137 register, whether they need a **Release** build (`NDEBUG`) or a **signed** sparse MSIX package,
138 and any Explorer-restart or first-run needs. Skipping this produces opaque failures — e.g. a
139 context-menu entry never appears because a Debug build compiles registration out, or an
140 unsigned `.msix` fails to register (`0x800B0100`).
141- [ ] 2. Read the two reference examples (ColorPicker .Next + ScreenRuler legacy) end-to-end
142- [ ] 3. Inventory the source:
143 • Scenario A → list every [TestMethod] + shared helper in the legacy project
144 • Scenario B → read the module's sign-off .md; list each manual checklist item
145 • For each workflow → list every external boundary (runner, Explorer, HWND, renderer,
146 compositor, child process) and its authoritative ready signal
147 — references/porting-workflow.md
148- [ ] 4. Internalize the deltas — references/framework-differences.md
149- [ ] 5. Scaffold the new project (csproj + PerMonitorV2 app.manifest from templates, name per the
150 table, register in .slnx)
151 — references/project-setup.md
152- [ ] 6. Re-implement tests, mapping each API as you go — references/api-mapping.md
153 + recipes from references/patterns-and-pitfalls.md
154- [ ] 6a. If Explorer/Shell is involved, apply references/explorer-shell-tests.md
155- [ ] 7. Apply the CI-stability checklist BEFORE building — references/ci-stability.md
156 (stable authoritative signals, retry classification, foreground/integrity, lifecycle reset
157 scope, non-activating helper processes, composed capture, DPI manifest, single-module enable,
158 first-run suppression)
159- [ ] 7a. If a test changes a module's enabled state through Settings, keep the real Settings UI +
160 immediate runtime assertion. Verify the selected UITest project is covered by the existing
161 `$requiresAuthenticatedSettingsIpc` companion-signing path in
162 `.pipelines/v2/templates/job-test-project.yml`; never add a test-side settings/restart fallback
163 for Release CI — [references/ci-stability.md](references/ci-stability.md#principle-5a--keep-module-lifecycle-tests-on-real-release-settings-ipc)
164- [ ] 8. Build the new project to exit code 0 — this SKILL.md "Build & validate"
165- [ ] 9. Run one deterministic test in the local VM and diagnose the first failure
166 — ../ui-tests-local-vm/SKILL.md
167- [ ] 10. Rerun the focused test after each fix, then widen to the complete module suite with bounded
168 timeouts; parse TRX and verify durable evidence export
169- [ ] 11. If the local VM is unavailable or unsupported, run on another live desktop or report the exact
170 environmental blocker; do not silently stop at compile validation
171```
172
173## Build & validate
174
175The `.Next` harness needs `winapp.exe` only at **run** time, not build time — the project has zero
176managed dependency on the engine. So you can always compile-verify a migration even on an agent with
177no winappcli installed.
178
179```pwsh
180# 0. FIRST build of a brand-new project: restore so the assets file exists, otherwise the build
181# fails with NETSDK1004 "Assets file ... project.assets.json not found".
182dotnet restore src\modules\<Module>\Tests\<Module>.UITests.Next\<Module>.UITests.Next.csproj -p:Platform=x64
183# (Equivalently, run tools\build\build-essentials.cmd once at the start of the session.)
184
185# 1. Build just the new test project (fast inner loop). Prefer the repo build script.
186tools\build\build.cmd -Path src\modules\<Module>\Tests\<Module>.UITests.Next -Platform x64 -Configuration Debug
187# Exit code 0 = success; non-zero = failure. On failure read the errors log next to the project:
188# build.<Configuration>.<Platform>.errors.log
189# Do not substitute `dotnet build` when UITestAutomation.Next's COM references are in the graph:
190# .NET SDK MSBuild cannot run ResolveComReference and fails with MSB4803. Use the repo script or
191# Visual Studio's full-framework MSBuild.exe; use `dotnet restore` only to create project.assets.json.
192
193# 2. Run (needs a live desktop). A .Next project is a Microsoft.Testing.Platform Exe — run the
194# produced exe directly with a TRX report; filter to one test/category for a tight loop.
195$exe = "<repo>\x64\Debug\tests\<Module>.UITests.Next\net10.0-windows10.0.26100.0\<Module>.UITests.Next.exe"
196& $exe --filter "TestCategory=<Cat>" --report-trx --report-trx-filename run.trx --results-directory <dir>
197# --filter accepts "TestCategory=X" or "FullyQualifiedName~Y"; omit it to run everything.
198# Exit 0 = all passed. Parse the .trx for per-test outcomes + failure messages.
199```
200
201- **Default to persistent local VM validation —
202 [ui-tests-local-vm](../ui-tests-local-vm/SKILL.md).** It keeps the interactive desktop and staged
203 tools, refreshes only changed archives, and returns durable status/TRX/evidence. Do not
204 modify stabilized tests merely to improve a VM-specific pass rate when the task only asks whether
205 the execution loop works. Finish clean-profile claims from a restored known baseline or a fresh
206 named VM volume.
207- **Design for CI stability up-front — [references/ci-stability.md](references/ci-stability.md).**
208 Before the first push, walk its pre-flight checklist (authoritative-signal retries instead of fixed
209 sleeps, navigation via UIA invoke, interaction-scoped foreground checks, non-activating helpers,
210 Win32 window/overlay detection, screen-capture cold-start handling, DPI manifest, single-module
211 enable, first-run suppression). Most "passes local, fails CI" loops come from skipping one of
212 these; applying them proactively is how you spend one CI iteration instead of six.
213- **Run it in a loop: write → build → run → diagnose → repeat.** UI tests surface environment-real
214 failures (DPI scaling, cursor position, hotkey-arming races) that only a live run reveals. Start
215 with one deterministic test (e.g. the activation/toggle test), get it green, then widen.
216- **Diagnose from the artifacts, not from the assertion message.** Every failed test attaches a
217 desktop screenshot, and in pipeline mode an MP4 of the run. **Open them before forming any theory**
218 — especially before concluding the product is broken. An assertion can only say "found 0 rows"; the
219 screenshot says whether the list was empty or whether your selector was wrong. This is the single
220 highest-leverage habit in the agentic loop: skipping it cost ~8 iterations and a confident but
221 entirely wrong product-defect report on File Locksmith (see
222 [references/patterns-and-pitfalls.md](references/patterns-and-pitfalls.md) Pitfall 26). If there is
223 no video, find out why rather than proceeding blind — the harness now prints the reason (a clean
224 Windows image without the Visual C++ redistributable cannot load the native encoder).
225- **First, run the *legacy* suite once for a baseline — and run it ELEVATED.** The legacy harness
226 launches PowerToys via `ProcessStartInfo { Verb = "runas" }` (elevated), so a **non-elevated** test
227 host can't complete the launch and **every test fails at startup with a misleading `Win32Exception`
228 cascade** — a false 0/N that looks like "the tests are broken" but is purely the run method. (That's
229 why VS Test Explorer passes them: VS runs as admin.) Run from an **elevated** terminal: start
230 `WinAppDriver.exe` on `127.0.0.1:4723`, then run the built DLL with `vstest.console.exe` (see
231 [references/porting-workflow.md](references/porting-workflow.md) §A0 for the `-Verb RunAs` recipe).
232 A measurement failure on a scaled (non-100%) display is usually a pre-existing DPI issue (Pitfall
233 12), not something the port must reproduce — the ScreenRuler legacy suite scores **4/5** elevated
234 here (Bounds fails at 150% scale) while the `.Next` port scores **5/5**. `.Next` tests themselves
235 need **no** elevation (the new harness launches the runner non-elevated).
236- **Always** build to exit code 0 before declaring the migration done. Fix every compile error — do
237 not leave `// TODO: port this` stubs that break the build.
238- Running the tests requires a **live interactive desktop** plus `winapp.exe`
239 (`winget install Microsoft.winappcli`, or set `WINAPP_CLI_PATH`). The whole PowerToys runner is
240 launched by the harness (`PowerToys.exe --open-settings`) — you should see the Settings window
241 appear. If the environment has no desktop (headless agent), state that the project **builds clean
242 and is ready to run**, and list which source tests/checklist items each new `[TestMethod]` covers.
243- New `.csproj` files under `src/` MUST `<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />`
244 right after `<Project Sdk=...>` (CI audits this). The template already does.
245
246## What NOT to do
247
248- **Do NOT delete or edit the legacy `[Module].UITests` project** in Scenario A. The `.Next` project
249 lives alongside it; removing the old one is a separate, explicit decision for the maintainers.
250- **Do NOT change product behaviour.** This is a test-only migration. The one sanctioned product edit
251 is adding **`AutomationProperties.AutomationId`** to a control that is otherwise unaddressable — an
252 icon-only button whose label lives in a tooltip has no UIA Name at all, and the alternative is a
253 brittle coordinate click. Use `AutomationProperties.AutomationId`, never `x:Name` (which also emits
254 a code-behind field); see [references/patterns-and-pitfalls.md](references/patterns-and-pitfalls.md)
255 Recipe 16. Anything larger — a hidden automation-peer TextBlock, a new property, a state string —
256 must be flagged for the user instead. (ColorPicker's `ColorHexAutomationPeer` hook is a documented,
257 pre-existing exception — see its class remarks.)
258- **Do NOT port the legacy plumbing literally.** No Selenium `Actions`, no `WindowsDriver`/`WindowsElement`,
259 no `By.XPath`/`By.CssSelector`, no `:4723`. Map them to the winappcli idioms in
260 [references/api-mapping.md](references/api-mapping.md).
261- **Do NOT add a `ProjectReference` to `UITestAutomation.csproj`** (the legacy harness) — reference
262 **`UITestAutomation.Next.csproj`** only.
263- **Do NOT invent assertions** for a vague sign-off item. If a checklist line has no observable
264 pass/fail signal, implement what you can and leave a clearly-marked `TestContext.WriteLine` note
265 (or skip with an explanation) rather than asserting on something you can't actually read.
266- **Do NOT introduce new third-party NuGet dependencies.** The `.Next` harness is intentionally
267 dependency-free (MSTest only). Use the Win32-based helpers it already ships.
268- **Do NOT retry a toggle hotkey blindly.** Once any target window appears, wait for initialization;
269 resending the chord may close a healthy window. Restart only after a terminal readiness failure.
270- **Do NOT replace or weaken visual baselines before proving capture is correct.** Foreground HWND,
271 DWM z-order, composed WebView content, theme, and platform are separate failure sources.
272- **Do NOT work around a Release Runner `not-microsoft-signed` Settings rejection in test code.** Do
273 not seed the global enabled map, restart PowerToys, bypass authentication, or weaken the lifecycle
274 assertion. Reuse the pipeline's existing Runner/Settings companion-signing mechanism; see
275 [references/ci-stability.md](references/ci-stability.md#principle-5a--keep-module-lifecycle-tests-on-real-release-settings-ipc).
276
277## What is NICE to do
278
279- **Improve the framework when a helper is demonstrably reusable.** Prefer small composable APIs
280 (`WaitHelper`, `WindowControl`, `ExplorerShell`) over module-specific mega-helpers. Keep product
281 semantics such as Peek pin-state preservation in the module test.