Files
PowerToys/.github/skills/ui-tests-migration/references/project-setup.md
Gleb Khmyznikov e48152c52d [UITests] Add UITest.Next suites (Image Resizer, Peek, File Explorer add‑ons, File Locksmith) + local‑VM tooling and CI test‑signing (#49671)
## 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.
2026-08-10 19:55:03 +02:00

10 KiB

Project setup & scaffolding

How to create, place, name, register, and build the new .Next test project. The starter files live in ../templates/.

1. Decide the name and location

Scenario Project name Folder
A — Port (legacy UI tests exist) [Module].UITests.Next src/modules/[Module]/Tests/[Module].UITests.Next/
B — Greenfield (no UI tests) [Module].UITests src/modules/[Module]/Tests/[Module].UITests/

Rules and judgment:

  • The .Next suffix exists only to avoid colliding with an existing legacy project. If there is nothing to live alongside (Scenario B), drop it.
  • Match the module's existing test layout. Many modules already nest tests under a Tests/ folder (MeasureTool/Tests/ScreenRuler.UITests, LightSwitch/Tests/LightSwitch.UITests); others put the UI-tests project directly under the module root (colorPicker/ColorPicker.UITests, fancyzones/FancyZones.UITests). Mirror whatever the module already does — don't invent a new structure. The path-segment count only changes the relative ..\ depth to common\ in the csproj.
  • Keep the AssemblyName matching the project name ([Module].UITests.Next) so logs and build artifacts are unambiguous; there's no need to strip the .Next from the assembly name.
  • If the legacy project has an unusual file name (e.g. HostsEditor.UITests.csproj inside a Hosts.UITests/ folder), prefer a clean [Module].UITests.Next.csproj; consistency with the new examples (ColorPicker.UITests.csproj, Settings.UITests.csproj) wins.

2. Scaffold the csproj

Copy ../templates/Module.UITests.Next.csproj and replace the __MODULE__ placeholder (and fix the ..\ depth on the ProjectReference). The reference csproj (ColorPicker, whose project folder sits 3 levels under src/) is:

<Project Sdk="Microsoft.NET.Sdk">
  <!-- Look at Directory.Build.props in root for common stuff as well -->
  <Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
    <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
    <RootNamespace>Microsoft.__MODULE__.UITests</RootNamespace>
    <AssemblyName>__MODULE__.UITests.Next</AssemblyName>

    <!-- Microsoft.Testing.Platform: appears in Test Explorer AND runs via dotnet test / vstest. -->
    <IsTestingPlatformApplication>true</IsTestingPlatformApplication>
    <EnableMSTestRunner>true</EnableMSTestRunner>
    <GenerateDocumentationFile>false</GenerateDocumentationFile>

    <!-- UI tests need a live desktop; never run them as part of MSBuild. -->
    <RunVSTest>false</RunVSTest>
  </PropertyGroup>

  <!-- Stage the built test app under <Platform>\<Configuration>\tests\ so the UI-tests build
       pipeline (CopyFiles glob **/<plat>/<config>/tests/**) picks it up. -->
  <PropertyGroup>
    <OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\tests\__MODULE__.UITests.Next\</OutputPath>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="MSTest" />
  </ItemGroup>

  <ItemGroup>
    <!-- Adjust the ..\ depth to reach src\common from THIS project's folder. -->
    <ProjectReference Include="..\..\..\common\UITestAutomation.Next\UITestAutomation.Next.csproj" />
  </ItemGroup>
</Project>

Critical, non-negotiable bits (CI audits or the build will fail without them):

  1. <Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" /> immediately after the <Project Sdk=...> line. .pipelines/verifyCommonProps.ps1 requires it on every src/** csproj.
  2. OutputType=Exe, IsTestingPlatformApplication=true, EnableMSTestRunner=true — the Microsoft.Testing.Platform runner the rest of the repo uses; this is what makes the class appear in Test Explorer and run via dotnet test/vstest.console.exe.
  3. <OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\tests\<Name>\</OutputPath> — stages the build output where the UI-tests pipeline globs (**/<plat>/<config>/tests/**). Without it the app builds to bin\ and is never picked up by the test job.
  4. RunVSTest=false — UI tests must not run during MSBuild.
  5. ProjectReference to UITestAutomation.Next.csproj only — never the legacy UITestAutomation.csproj. Fix the ..\ depth to match the folder nesting:
    • src/modules/<M>/Tests/<M>.UITests.Next/ (4 levels under src) → ..\..\..\..\common\UITestAutomation.Next\UITestAutomation.Next.csproj
    • src/modules/<M>/<M>.UITests/ (3 levels under src) → ..\..\..\common\UITestAutomation.Next\UITestAutomation.Next.csproj
    • src/settings-ui/<M>.UITests/ (2 levels under src) → ..\..\common\UITestAutomation.Next\UITestAutomation.Next.csproj

Use MSTest (the meta-package) for a test Exe, matching the ColorPicker/Settings examples — not the bare MSTest.TestFramework the harness library itself uses.

3. Register in PowerToys.slnx

Add the project to ../../../../PowerToys.slnx inside the module's <Folder>, right next to the legacy project (Scenario A) so they're visually paired:

<Project Path="src/modules/<Module>/Tests/<Module>.UITests.Next/<Module>.UITests.Next.csproj">
  <Platform Solution="*|ARM64" Project="ARM64" />
  <Platform Solution="*|x64" Project="x64" />
</Project>

Match the <Platform> mapping block of the sibling projects in the same folder (every UI-tests entry uses the *|ARM64 → ARM64 / *|x64 → x64 pair shown above).

4. Add the test class(es) and shared helper

Copy ../templates/ModuleEndToEndTests.cs into the project, rename it to [Module]EndToEndTests.cs (or keep the legacy test-class names in Scenario A), and start filling in test methods.

For anything beyond a single trivial test, also copy ../templates/TestHelper.cs — a static helper with the reusable building blocks every port needs (navigate to the page, toggle + verify the process, read the activation shortcut, discover/activate/close the module window with patient retry, clipboard, screen-center). Fill in the __MODULE__ / __MODULEUI__ / AutomationId placeholders and delete what you don't use. This mirrors how the legacy suites are organized (a TestHelper + thin test classes) and is exactly the shape of the validated ScreenRuler port.

The standard file header is required on every .cs:

// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

4b. (Coordinate-exact tests only) add a DPI-aware app.manifest

If any test drives the mouse by pixel coordinates and asserts on an exact value (a drag that must measure 100 x 100, a click at a precise point), the test host MUST be per-monitor DPI aware, otherwise MouseHelper's SetCursorPos/GetCursorPos are virtualized by the display scale and stop matching winappcli's physical-pixel bounds (a 99px drag measured ~149px on a 150% display).

Copy ../templates/app.manifest into the project (or the one from the module's legacy UITests project) and reference it in the csproj:

<PropertyGroup>
  <ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>

Tests that only assert on format (regex like \d+ x \d+) or never touch raw coordinates don't need the manifest — which is why ColorPicker/Settings .Next projects omit it.

4c. (Visual tests only) embed platform baselines

Add baseline PNGs as embedded resources and use VisualAssert.AreEqual:

<ItemGroup>
  <EmbeddedResource Include="Baseline\*.png" />
</ItemGroup>

Resource names must end with the scenario generated by VisualAssert: <Class>_<CallingMethod>[_<Subname>]_<platform>.png. The pipeline platform values distinguish targets such as x64Win10, x64Win11, and arm64. Keep separate valid baselines where rendering really differs; do not regenerate them to hide capture, theme, DPI, or readiness defects.

Visual comparison runs only when EnvironmentConfig.IsInPipeline is true (TF_BUILD or platform is set). Set TF_BUILD=true and a representative platform locally when debugging pipeline capture. Session.ScreenshotVisibleWindow is the correct API for composed WinUI/WebView content.

4d. Explorer-driven tests need no project COM references

Use ExplorerShell from UITestAutomation.Next. The framework embeds Shell32/SHDocVw interop and does not expose COM types in its public API, so consuming test projects should not add their own COMReference items. Use SetSelectionAndWaitForStable for selected/focused paths and SetViewModeAndIconSizeAndWait for deterministic icon/list layout; do not duplicate Shell COM interop in the test project.

5. Build & run

# 0. FIRST build of a new project: restore so project.assets.json exists (else NETSDK1004).
dotnet restore src\modules\<Module>\Tests\<Module>.UITests.Next\<Module>.UITests.Next.csproj -p:Platform=x64
#    (or run tools\build\build-essentials.cmd once at the start of the session.)

# 1. Build only this project (fast). Exit code 0 = success.
tools\build\build.cmd -Path src\modules\<Module>\Tests\<Module>.UITests.Next -Platform x64 -Configuration Debug

# 2. Run (needs a live desktop + winapp.exe). A .Next project is a Microsoft.Testing.Platform Exe,
#    so run the produced exe directly (Test Explorer also works). Filter + TRX report for a tight loop:
$exe = "$PWD\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 .\TestResults\<Module>
#    --filter accepts "TestCategory=X" or "FullyQualifiedName~Y"; omit it to run everything. Exit 0 = all passed.
  • On build failure, read build.<Configuration>.<Platform>.errors.log next to the project.
  • winapp.exe is a run-time prerequisite only (winget install Microsoft.winappcli, or set WINAPP_CLI_PATH). A migration that compiles clean is valid even where the CLI/desktop is absent; say so and list coverage.
  • dotnet test also works for a one-shot run, but prefer the produced exe for a fast iterate loop and do not run UI tests from inside an MSBuild step — they need an interactive session.