## 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.
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
.Nextsuffix 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 tocommon\in the csproj. - Keep the
AssemblyNamematching the project name ([Module].UITests.Next) so logs and build artifacts are unambiguous; there's no need to strip the.Nextfrom the assembly name. - If the legacy project has an unusual file name (e.g.
HostsEditor.UITests.csprojinside aHosts.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):
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />immediately after the<Project Sdk=...>line..pipelines/verifyCommonProps.ps1requires it on everysrc/**csproj.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 viadotnet test/vstest.console.exe.<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 tobin\and is never picked up by the test job.RunVSTest=false— UI tests must not run during MSBuild.- ProjectReference to
UITestAutomation.Next.csprojonly — never the legacyUITestAutomation.csproj. Fix the..\depth to match the folder nesting:src/modules/<M>/Tests/<M>.UITests.Next/(4 levels undersrc) →..\..\..\..\common\UITestAutomation.Next\UITestAutomation.Next.csprojsrc/modules/<M>/<M>.UITests/(3 levels undersrc) →..\..\..\common\UITestAutomation.Next\UITestAutomation.Next.csprojsrc/settings-ui/<M>.UITests/(2 levels undersrc) →..\..\common\UITestAutomation.Next\UITestAutomation.Next.csproj
Use
MSTest(the meta-package) for a test Exe, matching the ColorPicker/Settings examples — not the bareMSTest.TestFrameworkthe 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.lognext to the project. winapp.exeis a run-time prerequisite only (winget install Microsoft.winappcli, or setWINAPP_CLI_PATH). A migration that compiles clean is valid even where the CLI/desktop is absent; say so and list coverage.dotnet testalso 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.