Files
PowerToys/doc/devdocs/development/ui-tests.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

12 KiB

UI tests framework

PowerToys provides UI-test frameworks for modules and Settings. New tests should use Microsoft.PowerToys.UITest.Next, which drives Windows UI Automation through winappcli and runs as a Microsoft.Testing.Platform executable. The legacy Microsoft.PowerToys.UITest framework uses WinAppDriver/Selenium and remains documented for existing suites and migration baselines.

Agent-assisted workflows

Two repository skills cover the complete implementation and validation loop:

  • UI-tests migration skill: create new .Next test projects, port legacy WinAppDriver tests, design stable selectors/waits/lifecycle, and prepare tests for CI.
  • Local-VM UI-tests skill: create persistent Windows 10 and Windows 11 Hyper-V guests, stage current build/test artifacts, execute tests in a standard-user interactive desktop, and collect durable TRX/log/screenshot/video evidence.

For new or migrated tests, use both skills. Build first, then use the local VMs as the default live agentic loop: run one deterministic test, diagnose and fix it, and finally widen to the complete module suite on both supported Windows versions.

Before running tests

.Next tests

  • Build the PowerToys runtime and .UITests.Next test executable.
  • Install the pinned winappcli runtime or set WINAPP_CLI_PATH. The pipeline helper is .pipelines/InstallWinAppCli.ps1.
  • Use a live interactive desktop. UIA, foreground input, Explorer, hotkeys, and rendering do not work in session 0.
  • Exit an existing PowerToys instance before a host-desktop run. The harness owns the runner and module lifecycle.

Legacy tests

Running tests

.Next tests

Build the focused project with the repository script, then run the produced Microsoft.Testing.Platform executable directly:

tools\build\build.cmd `
  -Path src\modules\<Module>\Tests\<Module>.UITests.Next `
  -Platform x64 `
  -Configuration Debug

$exe = 'x64\Debug\tests\<Module>.UITests.Next\net10.0-windows10.0.26100.0\<Module>.UITests.Next.exe'
& $exe `
  --filter 'TestCategory=<Module>' `
  --report-trx `
  --report-trx-filename module.trx `
  --results-directory .\TestResults\<Module> `
  --timeout 7m

Use explicit filter properties such as Name=, Name~, FullyQualifiedName~, or TestCategory=. A bare display name can select zero tests. The 7m timeout above is a focused-filter example; choose a larger value for a module or project-wide run.

Legacy tests

  • Exit PowerToys if it's running.

  • Open PowerToys.slnx in Visual Studio and build the solution.

  • Run tests in the Test Explorer (Test > Test Explorer or Ctrl+E, T).

Running .Next tests in persistent local VMs

The supported local backend is a pair of persistent Hyper-V guests driven through PowerShell Direct: Windows 10 and Windows 11, each with an already logged-on standard-user desktop. The VMs reveal first-run, profile, Explorer, WebView2, foreground, and lifecycle assumptions without modifying the host profile, while retaining staged payloads for a fast edit/build/rerun loop.

One-time host setup

Scaffold a VM root outside the repository, then follow the generated next steps to create the untracked configuration:

pwsh .github\skills\ui-tests-local-vm\scripts\Initialize-LocalVm.ps1 `
  -DestinationRoot C:\PowerToysUiTestVm

pwsh .github\skills\ui-tests-local-vm\scripts\Initialize-LocalVmHost.ps1 `
  -VmRoot C:\PowerToysUiTestVm `
  -CheckOnly

If -CheckOnly reports IsReady=false, a human must run the elevated setup command it prints. Hyper-V group membership, the DPAPI-protected guest administrator credential, and guest creation cannot be completed by an agent. See the setup reference for install media, vm.config.psd1, Windows 10/11 guest creation, and baseline checkpoints.

Run the agentic loop

Create a module exchange containing ui-tests.zip, powertoys-runtime.zip, winappcli.zip, and dotnet-runtime.zip as described in the agentic-loop reference. Payloads are extracted to guest-local storage; tests are never run directly from a host share.

$vmRoot = 'C:\PowerToysUiTestVm'
$exchange = "$vmRoot\shared\PowerToysUiTests\<Module>"

pwsh .github\skills\ui-tests-local-vm\scripts\Invoke-LocalVmUiTest.ps1 `
  -VmName PowerToysUiTest-Win11 `
  -ConfigurationPath "$vmRoot\vm.config.psd1" `
  -VmRoot $vmRoot `
  -ExchangeRoot $exchange `
  -TestExecutable '<Module>.UITests.Next.exe' `
  -Filter 'Name=<Module>.FocusedTest' `
  -Platform x64Win11 `
  -BuildLabel (git rev-parse HEAD) `
  -SuiteTimeout 15m `
  -TimeoutMinutes 25 `
  -ReuseStagedPayload

The controller starts the guest if needed, validates the standard-user token, Explorer session, and desktop size, then runs the test through a limited interactive scheduled task. It streams progress and returns status.json, TRX counters, per-test failures, logs, screenshots, and retained failure recordings under <ExchangeRoot>\LocalVmResults\<runId>.

After each source change, rebuild and replace only the changed archive, then rerun the same focused filter with -ReuseStagedPayload. Widen only after that behavior is understood. A module is complete only after the full category filter passes with executed == total on both Windows 10 and Windows 11; restore the baseline checkpoint for the final clean-profile confirmation. See the local-VM troubleshooting guide for desktop, PowerShell Direct, payload, shell-extension signing, and evidence failures.

Running tests in pipeline

The PowerToys UI test pipeline provides flexible options for building and testing:

Pipeline Options

  • buildSource: Select the build type for testing:

    • latestMainOfficialBuild: Downloads and uses the latest official PowerToys build from main branch
    • buildNow: Builds PowerToys from current source code and uses it for testing
    • specificBuildId: Downloads a specific PowerToys build using the build ID specified in specificBuildId parameter

    Default value: latestMainOfficialBuild

  • specificBuildId: When buildSource is set to specificBuildId, specify the exact PowerToys build ID to download and test against.

    Default value: "xxxx" (placeholder, enter actual build ID when using specificBuildId option)

    When to use this:

    • Testing against a specific known build for reproducibility
    • Regression testing against a particular build version
    • Validating fixes in a specific build before release

    Usage: Enter the build ID number (e.g., 12345) to download that specific build. Only used when buildSource is set to specificBuildId.

  • uiTestModules: Specify which UI test modules to build and run. This parameter controls both the .csproj projects to build and the .dll test assemblies to execute. Examples:

    • ['UITests-FancyZones'] - Only FancyZones UI tests
    • ['MouseUtils.UITests'] - Only MouseUtils UI tests
    • ['UITests-FancyZones', 'MouseUtils.UITests'] - Multiple specific modules
    • Leave empty to build and run all UI test modules

    Important: The uiTestModules parameter values must match both the test project names (for .csproj selection during build) and the test assembly names (for .dll execution during testing).

Build Modes

  1. Official Build Testing (buildSource = latestMainOfficialBuild or specificBuildId)

    • Downloads and installs official PowerToys build (latest from main or specific build ID)
    • Builds only UI test projects (all or specific based on uiTestModules)
    • Runs UI tests against installed PowerToys
    • Tests both machine-level and per-user installation modes automatically
  2. Current Source Build Testing (buildSource = buildNow)

    • Builds entire PowerToys solution from current source code
    • Builds UI test projects (all or specific based on uiTestModules)
    • Runs UI tests against freshly built PowerToys
    • Uses artifacts from current pipeline build

Note

: All modes support the uiTestModules parameter to control which specific UI test modules to build and run. Both machine-level and per-user installation modes are tested automatically when using official builds.

Pipeline Access

How to add the first UI tests for your modules

Use the UI-tests migration skill for new .Next projects and ports. It contains the current executable project scaffold, API mapping, naming, CI-stability checklist, and validated examples.

The project sample below describes the legacy WinAppDriver framework and is retained for existing legacy suites. Do not use it as the starting point for a new .Next project.

  • Follow the naming convention: {ModuleFolder}/Tests/{ModuleName}-{TestType(Fuzz/UI/Unit)}Tests

  • Create a new project and add the following references to the project file. Change the OutputPath to your own module's path.

      <Project Sdk="Microsoft.NET.Sdk">
      <!-- Look at Directory.Build.props in root for common stuff as well -->
      <Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
    
      <PropertyGroup>
          <ProjectGuid>{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}</ProjectGuid>
          <RootNamespace>PowerToys.Hosts.UITests</RootNamespace>
          <AssemblyName>PowerToys.Hosts.UITests</AssemblyName>
          <IsPackable>false</IsPackable>
          <IsTestProject>true</IsTestProject>
          <Nullable>enable</Nullable>
          <OutputType>Library</OutputType>
    
          <!-- This is a UI test, so don't run as part of MSBuild -->
          <RunVSTest>false</RunVSTest>
          </PropertyGroup>
          <PropertyGroup>
          <OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\Hosts.UITests\</OutputPath>
          </PropertyGroup>
    
          <ItemGroup>
          <PackageReference Include="MSTest" />
          <ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
          </ItemGroup>
      </Project>
    
    
  • Inherit your test class from UITestBase.

    Set Scope: The default scope starts from the PowerToys settings UI. If you want to start from your own module, set the constructor as shown below:

    Specify Scope:

      [TestClass]
      public class HostModuleTests : UITestBase
      {
          public HostModuleTests()
              : base(PowerToysModule.Hosts, WindowSize.Small_Vertical)
          {
          }
      }
    
  • Then you can start performing the UI operations.

Example

[TestMethod("Hosts.Basic.EmptyViewShouldWork")]
[TestCategory("Hosts File Editor #4")]
public void TestEmptyView()
{
    this.CloseWarningDialog();
    this.RemoveAllEntries();

    // 'Add an entry' button (only show-up when list is empty) should be visible
    Assert.IsTrue(this.HasOne<HyperlinkButton>("Add an entry"), "'Add an entry' button should be visible in the empty view");

    VisualAssert.AreEqual(this.TestContext, this.Find("Entries"), "EmptyView");

    // Click 'Add an entry' from empty-view for adding Host override rule
    this.Find<HyperlinkButton>("Add an entry").Click();

    this.AddEntry("192.168.0.1", "localhost", false, false);

    // Should have one row now and not more empty view
    Assert.IsTrue(this.Has<Button>("Delete"), "Should have one row now");
    Assert.IsFalse(this.Has<HyperlinkButton>("Add an entry"), "'Add an entry' button should be invisible if not empty view");

    VisualAssert.AreEqual(this.TestContext, this.Find("Entries"), "NonEmptyView");
}

Extra tools and information

Accessibility Tools: While working on tests, you may need a tool that helps you to view the element's accessibility data, e.g. for finding the button to click. For this purpose, you could use AccessibilityInsights.