Skip to content

[UITests] Add UITest.Next suites (Image Resizer, Peek, File Explorer add‑ons) + local‑VM tooling and CI test‑signing - #49671

Open
khmyznikov wants to merge 48 commits into
mainfrom
gleb/uitests-image-resize
Open

[UITests] Add UITest.Next suites (Image Resizer, Peek, File Explorer add‑ons) + local‑VM tooling and CI test‑signing#49671
khmyznikov wants to merge 48 commits into
mainfrom
gleb/uitests-image-resize

Conversation

@khmyznikov

@khmyznikov khmyznikov commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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: #40660 #49424 #40661

What's added

New UI test suites

  • Image Resizersrc/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.
  • Peeksrc/modules/peek/Peek.UITests.Next: file‑preview coverage across image/text/archive/
    markdown types with per‑arch visual baselines.
  • File Explorer add‑onssrc/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.

@khmyznikov khmyznikov added Area-Tests issues that relate to tests Product-Image Resizer Things regarding image resizing module Product-File Explorer Power Toys that touch explorer like Preview Pane Product-Peek Refers to Peek Powertoys labels Aug 4, 2026
@khmyznikov khmyznikov self-assigned this Aug 4, 2026
This was linked to issues Aug 4, 2026
@khmyznikov khmyznikov moved this to In Progress in UI Tests tracker Aug 4, 2026
@khmyznikov
khmyznikov marked this pull request as ready for review August 4, 2026 21:18
@khmyznikov
khmyznikov requested a review from a team as a code owner August 4, 2026 21:18
& "$(build.sourcesdirectory)\.pipelines\signSparsePackages.ps1" -PackageRoot @(
"$(Pipeline.Workspace)\$(TestArtifactsName)",
"$env:ProgramFiles\PowerToys\WinUI3Apps",
"$env:LOCALAPPDATA\PowerToys\WinUI3Apps")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sparse-package setup a required precondition for Image Resizer jobs

Severity: high

This step treats signing/trust failures as best-effort, but the new Windows 11 Image Resizer test explicitly has no classic-menu fallback and there is no ModernRegistered() guard in the suite. A signing failure is therefore reported later as a misleading product/test failure; additionally, the installed roots passed here omit ImageResizerContextMenuPackage.msix, which is installed at the PowerToys root rather than under WinUI3Apps. Make the Image Resizer/all-modules jobs require a successfully trusted ImageResizerContextMenuPackage.msix, search the PowerToys install roots recursively, and have signSparsePackages.ps1 throw when the required package cannot be signed or trusted.

Comment on lines +38 to +55
[int]$WinRmPort = 15987,
[switch]$UseHttp,
[string]$CredentialPath = (Join-Path $env:LOCALAPPDATA 'PowerToysUiTestVm-Win11\admin.credential.xml')
)

$ErrorActionPreference = 'Stop'
if (-not (Test-Path $CredentialPath)) {
throw "Credential file not found: $CredentialPath. Point -CredentialPath at the VM's admin.credential.xml."
}
$credential = Import-Clixml $CredentialPath

if ($UseHttp) {
$session = New-PSSession -ConnectionUri "http://127.0.0.1:$WinRmPort/wsman" -Authentication Basic -Credential $credential
}
else {
$sessionOption = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$session = New-PSSession -ConnectionUri "https://127.0.0.1:$WinRmPort/wsman" -Authentication Basic -Credential $credential -SessionOption $sessionOption
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Align the guest helper with the shipped VM connection settings

Severity: medium

The scaffold/controller default to HTTPS on port 15986, while this helper defaults to 15987; its HTTP path also uses Basic authentication even though the controller and documentation use Negotiate. As written, the helper fails against a stock scaffold and the migration fallback uses the wrong authentication mode. Align the default port and session construction with Invoke-LocalVmUiTest.ps1.

Suggested change
[int]$WinRmPort = 15987,
[switch]$UseHttp,
[string]$CredentialPath = (Join-Path $env:LOCALAPPDATA 'PowerToysUiTestVm-Win11\admin.credential.xml')
)
$ErrorActionPreference = 'Stop'
if (-not (Test-Path $CredentialPath)) {
throw "Credential file not found: $CredentialPath. Point -CredentialPath at the VM's admin.credential.xml."
}
$credential = Import-Clixml $CredentialPath
if ($UseHttp) {
$session = New-PSSession -ConnectionUri "http://127.0.0.1:$WinRmPort/wsman" -Authentication Basic -Credential $credential
}
else {
$sessionOption = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$session = New-PSSession -ConnectionUri "https://127.0.0.1:$WinRmPort/wsman" -Authentication Basic -Credential $credential -SessionOption $sessionOption
}
param(
[Parameter(Mandatory)][scriptblock]$ScriptBlock,
[object[]]$ArgumentList = @(),
[int]$WinRmPort = 15986,
[switch]$UseHttp,
[string]$CredentialPath = (Join-Path $env:LOCALAPPDATA 'PowerToysUiTestVm-Win11\admin.credential.xml')
)
$ErrorActionPreference = 'Stop'
if (-not (Test-Path $CredentialPath)) {
throw "Credential file not found: $CredentialPath. Point -CredentialPath at the VM's admin.credential.xml."
}
$credential = Import-Clixml $CredentialPath
$scheme = if ($UseHttp) { 'http' } else { 'https' }
$authentication = if ($UseHttp) { 'Negotiate' } else { 'Basic' }
$sessionOption = if ($UseHttp) {
New-PSSessionOption
}
else {
New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
}
$session = New-PSSession `
-ConnectionUri "${scheme}://127.0.0.1:$WinRmPort/wsman" `
-Authentication $authentication `
-Credential $credential `
-SessionOption $sessionOption

- [UI-tests migration skill](../../../.github/skills/ui-tests-migration/SKILL.md): create new
`.Next` test projects, port legacy WinAppDriver tests, design stable selectors/waits/lifecycle, and
prepare tests for CI.
- [Windows Sandbox UI-tests skill](../../../.github/skills/windows-sandbox-ui-tests/SKILL.md): enable

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the non-existent Sandbox workflow with the shipped local-VM skill

Severity: medium

This guide links to .github/skills/windows-sandbox-ui-tests, but that skill and its scripts do not exist in this PR. The repository actually adds ui-tests-local-vm, so contributors following the documented .Next workflow hit missing paths at both this link and the later Sandbox commands. Rewrite this section and the “Running .Next tests” walkthrough to use ui-tests-local-vm/SKILL.md, Initialize-LocalVm.ps1, and Invoke-LocalVmUiTest.ps1.

Comment on lines +120 to +122
toggle = SetModuleEnabled(toggle, true);
explorer = OpenExplorer(folder);
AssertContextMenuPresence(explorer, new[] { fixture }, expected: true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refresh Explorer after re-enabling the context-menu handler

Severity: medium

EnsureContextMenuHandlerLoaded() documents that an existing Explorer process does not see runtime handler registration until the shell restarts, but its one-time guard has already been consumed by the disabled-state assertion. The re-enabled assertion can therefore fail even when Image Resizer registers correctly. Reset the guard so OpenExplorer performs a second refresh for the new registration state.

Suggested change
toggle = SetModuleEnabled(toggle, true);
explorer = OpenExplorer(folder);
AssertContextMenuPresence(explorer, new[] { fixture }, expected: true);
toggle = SetModuleEnabled(toggle, true);
contextMenuExplorerRefreshed = false;
explorer = OpenExplorer(folder);
AssertContextMenuPresence(explorer, new[] { fixture }, expected: true);

{
ForcePipelineLightTheme();

SettingsConfigHelper.UpdateModuleSettings(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restore module settings after the new UI-test suites finish

Severity: medium

This static setup rewrites the real Peek settings file, but class cleanup restores only the theme registry value. FileExplorerAddonsTests has the same issue for the File Explorer settings file. Because UI-test executables run sequentially in the same user profile—and local VMs are persistent—these changes leak into later suites and reruns, creating order-dependent behavior. Snapshot each module's settings.json before mutation and restore/delete it in class cleanup; a reusable SettingsConfigHelper.PreserveModuleSettings scope with unit coverage can serve both suites, matching the existing Image Resizer cleanup pattern.

{
try
{
if (browser.HWND != explorerWindow.ToInt64() || browser.Document is not Shell32.IShellFolderViewDual2 folderView)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Release the Shell folder-view COM objects used by polling helpers

Severity: medium

The selection/view helpers release each IWebBrowserApp, but browser.Document creates a separate folder-view RCW that is never released. These methods are called repeatedly by stable waits and recovery loops, so the leaked RCWs can accumulate and eventually cause flaky Shell COM failures. Capture browser.Document in a local variable and release it in the same finally block before releasing browser; apply the same pattern in TryGetSelection, TryGetView, TrySetView, and TrySetSelection (around lines 136, 226, 264, and 306).

Comment on lines +24 to +33
<TextBlock
x:Name="PreviewStateAutomationPeer"
Width="1"
Height="1"
HorizontalAlignment="Left"
VerticalAlignment="Top"
AutomationProperties.AutomationId="PreviewStateAutomationPeer"
IsHitTestVisible="False"
Opacity="0"
Text="{x:Bind GetPreviewStateText(Previewer.State), Mode=OneWay}" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the hidden test probe out of the screen-reader control view

Severity: medium

The transparent 1×1 TextBlock remains visible in the accessibility control view, so assistive technology can announce test-only preview-state text in production. Mark it as Raw so UI Automation tests can still query it without adding spoken content for users.

Suggested change
<TextBlock
x:Name="PreviewStateAutomationPeer"
Width="1"
Height="1"
HorizontalAlignment="Left"
VerticalAlignment="Top"
AutomationProperties.AutomationId="PreviewStateAutomationPeer"
IsHitTestVisible="False"
Opacity="0"
Text="{x:Bind GetPreviewStateText(Previewer.State), Mode=OneWay}" />
<TextBlock
x:Name="PreviewStateAutomationPeer"
Width="1"
Height="1"
HorizontalAlignment="Left"
VerticalAlignment="Top"
AutomationProperties.AccessibilityView="Raw"
AutomationProperties.AutomationId="PreviewStateAutomationPeer"
IsHitTestVisible="False"
Opacity="0"
Text="{x:Bind GetPreviewStateText(Previewer.State), Mode=OneWay}" />

Comment on lines +132 to +136
public static void MoveWindow(IntPtr hWnd, int x, int y)
{
var (left, top, right, bottom) = GetWindowBounds(hWnd);
SetWindowPos(hWnd, IntPtr.Zero, x, y, right - left, bottom - top, SWP_NOZORDER | SWP_NOACTIVATE);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preserve window size without depending on a successful bounds query

Severity: medium

GetWindowBounds returns (0,0,0,0) when GetWindowRect fails, so this helper can unexpectedly resize the target window to 0×0 while trying only to move it. SetWindowPos already supports preserving size directly; use SWP_NOSIZE and avoid the fragile read entirely.

Suggested change
public static void MoveWindow(IntPtr hWnd, int x, int y)
{
var (left, top, right, bottom) = GetWindowBounds(hWnd);
SetWindowPos(hWnd, IntPtr.Zero, x, y, right - left, bottom - top, SWP_NOZORDER | SWP_NOACTIVATE);
}
/// <summary>Move a window to explicit screen coordinates while preserving its current size.</summary>
public static void MoveWindow(IntPtr hWnd, int x, int y) =>
SetWindowPos(hWnd, IntPtr.Zero, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Tests issues that relate to tests Product-File Explorer Power Toys that touch explorer like Preview Pane Product-Image Resizer Things regarding image resizing module Product-Peek Refers to Peek Powertoys

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

UITests - Peek Migration UITests - Image Resizer UITests - File Explorer Add-ons

2 participants