# Add winappcli-based UI test harness (no WinAppDriver / Selenium) ## Summary Introduces a new UI test harness — `Microsoft.PowerToys.UITest.Next` — that drives PowerToys modules through Microsoft's [winappcli](https://github.com/microsoft/WinAppCli) (UI Automation CLI) instead of WinAppDriver + Selenium. Engine is a single executable shelled out from C#; no third-party NuGet packages, no driver process, no Appium server. Adds two real consumers: a full ColorPicker end-to-end scenario and a Settings shell navigation smoke test. This is opt-in and additive — the existing `UITestAutomation` library and the WinAppDriver-based test projects are untouched. Both can coexist while we evaluate the new harness. Inspired in part by [#48414](https://github.com/microsoft/PowerToys/pull/48414), which lands the same architectural bet (winappcli, AutomationId selectors, no WinAppDriver) at a smaller scope. This PR generalizes it into a reusable library. ## Why WinAppDriver + Selenium is a legacy pre-agentic solution that is no longer actively maintained. It's unreliable, heavyweight, and slow. To achieve 100% UI test coverage, we should leverage modern, reliable solutions, and WinApp CLI is a strong candidate. ## What's in this PR ### Harness library — [`src/common/UITestAutomation.Next/`](src/common/UITestAutomation.Next/) | File | Purpose | |---|---| | [`WinappCli.cs`](src/common/UITestAutomation.Next/WinappCli.cs) | Process wrapper around `winapp.exe`. `Invoke` / `InvokeAssertSuccess` / `InvokeJson` / `IsAvailable` / `TryResolveExecutable`. `Result` carries the args and emits `DescribeFailure()` like `winapp ui invoke X -w 12345 -> exit 1; stderr: ...` | | [`Session.cs`](src/common/UITestAutomation.Next/Session.cs) | Test session, scoped by either HWND (`-w`) or process (`-a`) via `TargetScope`. `Find<T>` / `FindAll<T>` / `Inspect` / `Screenshot` / `SendKeys`. `Session.FromProcess(...)` factory for the single-window-per-process case | | [`SessionHelper.cs`](src/common/UITestAutomation.Next/SessionHelper.cs) | Owns the launch + window-readiness flow. Static `EnsureRunning(scope, timeout)` returns whether the call had to launch (so cleanup only kills what we started). Uses `UseShellExecute=true` so child handles don't keep MSTest hanging | | [`UITestBase.cs`](src/common/UITestAutomation.Next/UITestBase.cs) | MSTest base class. Pre-flights `WinappCli.IsAvailable()` once per process and fails fast with the install hint if `winapp.exe` isn't on PATH | | [`Element/*.cs`](src/common/UITestAutomation.Next/Element/) | `Element`, `Button`, `ToggleSwitch`, `TextBox`, `NavigationViewItem`, `Window`. `Click` / `MouseClick` / `Focus` / `GetProperty` / `GetValue` / `HelpText` / `WaitForProperty` / `WaitForGone` plus coords (`X`/`Y`/`Width`/`Height`) | | [`By.cs`](src/common/UITestAutomation.Next/By.cs) | `By.Name` / `By.AccessibilityId` / `By.Id` / `By.Slug` | | [`Windows.cs`](src/common/UITestAutomation.Next/Windows.cs) | `WindowsFinder.ListAll` / `ListByApp` / `WaitForWindowByApp` / `WaitForWindowByProcess`. Notes the winappcli bug where unfiltered `list-windows` drops untitled windows | | [`WindowControl.cs`](src/common/UITestAutomation.Next/WindowControl.cs) | Tolerant Win32 helpers — `TryCloseByApp` / `TryFocusByApp` / `SafeCloseAndFocus` / `TryKillProcess` — for `finally` blocks | | [`KeyboardHelper.cs`](src/common/UITestAutomation.Next/KeyboardHelper.cs) | Hybrid `keybd_event` + `SendKeys.SendWait` chord sender — required for global PowerToys hotkeys | | [`MouseHelper.cs`](src/common/UITestAutomation.Next/MouseHelper.cs) | `MoveTo` / `LeftClick` / `RightClick` / `LeftClickAt` Win32 wrappers | | [`ClipboardHelper.cs`](src/common/UITestAutomation.Next/ClipboardHelper.cs) | STA-thread `Clipboard` access with `WaitForText` | | [`ModuleConfigData.cs`](src/common/UITestAutomation.Next/ModuleConfigData.cs) | `PowerToysModule` enum + path/process-name resolution | ### Tests **[`src/modules/colorPicker/ColorPicker.UITests/`](src/modules/colorPicker/ColorPicker.UITests/)** — replaces the previous empty `UITest-ColorPicker` stub. One test, [`ColorPickerEndToEndTests.NavigateReadShortcutActivateAndCapture`](src/modules/colorPicker/ColorPicker.UITests/ColorPickerEndToEndTests.cs), drives the full E2E: 1. Navigate to the Color Picker page via the dashboard utilities stack 2. Toggle the module OFF, verify `PowerToys.ColorPickerUI` exits; toggle ON, verify it respawns 3. Read the activation shortcut from the page's `ShortcutControl` (`EditButton.HelpText`) 4. Clear clipboard, park cursor, send the chord 5. Wait for the picker overlay window 6. Read the displayed HEX from a hidden XAML automation peer (see below) 7. Left-click to capture; assert the clipboard value matches the peer's HEX 8. Wait for the editor window and assert the captured color appears in its tree **[`src/settings-ui/Settings.UITests/`](src/settings-ui/Settings.UITests/)** — `SettingsNavigationSmokeTests.NavigationItem_NavigatesWithoutCrashing` is one `[TestMethod]` parameterized with `[DynamicData]`, producing 31 discrete results — one per `NavigationViewItem` in [`ShellPage.xaml`](src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml). For each item: navigate, settle 250ms, assert `PowerToys.Settings` is still alive. Catches FailFast regressions in `ShellViewModel.Frame_NavigationFailed` that pure-logic unit tests can't reach (the failure path needs a `NavigationFailedEventArgs` which is a sealed WinRT projection). ### Product change **[`src/modules/colorPicker/ColorPickerUI/Views/MainView.xaml`](src/modules/colorPicker/ColorPickerUI/Views/MainView.xaml)** — adds a hidden `TextBlock` automation peer: ```xml <TextBlock x:Name="ColorHexAutomationPeer" AutomationProperties.AutomationId="ColorHexAutomationPeer" IsHitTestVisible="False" Opacity="0" Text="{Binding ColorText}" /> ``` The visible `ColorTextBlock` has `AutomationProperties.Name="{Binding ColorName}"`, which masks the HEX value in the UIA tree (you see "White" instead of `#FFFFFF`). This zero-impact peer mirrors `ColorText` so tests can read the actually-displayed HEX. `Opacity=0` + `IsHitTestVisible=False` keep it out of the visual layout and out of accessibility focus. ### Project wiring - [`PowerToys.slnx`](PowerToys.slnx) — registers `UITestAutomation.Next` under `/common/`, `ColorPicker.UITests` under `/modules/colorpicker/Tests/`, and `Settings.UITests` under `/settings-ui/Tests/`. Original `UITest-ColorPicker` stub csproj removed. - [`.github/actions/spell-check/expect.txt`](.github/actions/spell-check/expect.txt) — adds `winapp` / `winappcli`. ### Not in this PR - No pipeline changes. `winapp.exe` is expected to be pre-staged on the test agent image. If it's missing, `UITestBase` fails the first test with the install hint (`winget install Microsoft.winappcli`) rather than producing 30 opaque per-test errors. - No changes to the legacy `UITestAutomation` library or any of the existing `*.UITests` projects. ## Validation - All three projects build clean on `x64|Debug` (empty `build.<config>.<plat>.errors.log`): - `src/common/UITestAutomation.Next/` - `src/modules/colorPicker/ColorPicker.UITests/` - `src/settings-ui/Settings.UITests/` - Both tests run in Test Explorer / `dotnet test` via Microsoft.Testing.Platform (already enabled repo-wide in `Directory.Build.props`). - Local runs: ColorPicker E2E green; Settings smoke green across all 31 nav items. - `winapp 0.3.2` from `winget install Microsoft.winappcli`. ## Notes for reviewers - **`UseShellExecute = true`** in `SessionHelper.EnsureRunning` is intentional — `false` makes child processes inherit the test host's stdin/stdout/stderr handles, which keeps MTP/Test Explorer marking the run as "in progress" until the spawned PowerToys exits. - **Process-scope (`-a`) targeting** in the Settings smoke test handles single-instance handoff: the EXE you launch may exit with code 0 immediately after signalling an existing owner, so the alive check uses `Process.GetProcessesByName` rather than the launcher PID. - **AutomationId-only selectors** in the Settings smoke list keep the test localization-independent. Parent groups have `SelectsOnInvoked="False"` and only expand on click — `Element.Click` tries `InvokePattern → TogglePattern → SelectionItemPattern → ExpandCollapsePattern` so the same call works for both leaves and groups. - **Untitled-window discovery**: filtered `winapp ui list-windows -a <name>` returns windows that the unfiltered call drops (e.g. ColorPicker editor). `WindowsFinder.ListByApp` uses the filtered form. Reported upstream. ## Before Merge - Add the `winappcli` install step to the UI-test pipeline.
9.7 KiB
Fuzzing Testing in PowerToys
Overview
Fuzzing is an automated testing technique that helps identify vulnerabilities and bugs by feeding random, invalid, or unexpected data into the application. This is especially important for PowerToys modules that handle file input/output or user input, such as Hosts File Editor, Registry Preview, and others.
PowerToys integrates Microsoft's OneFuzz service to systematically discover edge cases and unexpected behaviors that could lead to crashes or security vulnerabilities. Fuzzing testing is a requirement from the security team to ensure robust and secure modules.
Why Fuzzing Matters
- Security Enhancement: Identifies potential security vulnerabilities before they reach production
- Stability Improvement: Discovers edge cases that might cause crashes
- Automated Bug Discovery: Finds bugs that traditional testing might miss
- Reduced Manual Testing: Automates the process of testing with unusual inputs
Types of Fuzzing in PowerToys
PowerToys supports two types of fuzzing depending on the module's implementation language:
Setting Up .NET Fuzzing Tests
Step 1: Add a Fuzzing Test Project
Create a new test project within your module folder. Ensure the project name follows the format *.FuzzTests.
Step 2: Configure the Project
-
Set up a
.NET 10 (Windows)project- Note: OneFuzz's .NET fuzzing is runtime-agnostic (".NET Core targets are preferred") and keys off the build drop directory, so PowerToys fuzz projects target net10 like the rest of the repo. Older guidance pinned .NET 8; that is no longer required.
-
Add the required files to your fuzzing test project:
- Create fuzzing test code
- Add
OneFuzzConfig.jsonconfiguration file
Step 3: Configure OneFuzzConfig.json
The OneFuzzConfig.json file provides critical information for deploying fuzzing jobs. For detailed guidance, see the OneFuzzConfig V3 Documentation.
{
"fuzzers": [
{
"name": "YourModuleFuzzer",
"fuzzerLibrary": "libfuzzer-dotnet",
"targetAssembly": "YourModule.FuzzTests.dll",
"targetClass": "YourModule.FuzzTests.FuzzTestClass",
"targetMethod": "FuzzTest",
"FuzzingTargetBinaries": [
"YourModule.FuzzTests.dll"
]
}
],
"adoTemplate": [
{
"AssignedTo": "leilzh@microsoft.com",
"jobNotificationEmail": "PowerToys@microsoft.com"
}
],
"oneFuzzJobs": [
{
"projectName": "PowerToys",
"targetName": "YourModule",
"jobDependencies": {
"binaries": [
"PowerToys\\x64\\Debug\\tests\\YourModule.FuzzTests\\net10.0-windows10.0.26100.0\\**"
]
}
}
],
"configVersion": "3.0.0"
}
Key fields to update:
- Update the
targetAssembly,targetClass,targetMethod, andFuzzingTargetBinariesfields - Set the
AssignedToandjobNotificationEmailto your Microsoft email - Update the
projectNameandtargetNamefields - Define job dependencies pointing to your compiled fuzzing tests
Step 4: Configure the OneFuzz Pipeline
Modify the patterns in the job steps within job-fuzz.yml to match your fuzzing project name:
- download: current
displayName: Download artifacts
artifact: $(ArtifactName)
patterns: |-
**/tests/*.FuzzTests/**
Setting Up C++ Fuzzing Tests
Step 1: Create a New C++ Project
- Use the Empty Project template
- Name it
<ModuleName>.FuzzingTest
Step 2: Update Build Configuration
- In Configuration Manager, uncheck Build for both Release|ARM64, Debug|ARM64 and Debug|x64 configurations
- ARM64 is not supported for fuzzing tests
Step 3: Enable ASan and libFuzzer in .vcxproj
Edit the project file to enable fuzzing:
<PropertyGroup>
<EnableASAN>true</EnableASAN>
<EnableFuzzer>true</EnableFuzzer>
</PropertyGroup>
Step 4: Add Fuzzing Compiler Flags
Add these to AdditionalOptions under the Fuzzing configuration:
/fsanitize=address
/fsanitize-coverage=inline-8bit-counters
/fsanitize-coverage=edge
/fsanitize-coverage=trace-cmp
/fsanitize-coverage=trace-div
%(AdditionalOptions)
Step 5: Link the Sanitizer Coverage Runtime
In Linker → Input → Additional Dependencies, add:
$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib
Step 6: Copy Required Runtime DLL
Add a PostBuildEvent to copy the ASAN DLL:
<Command>
xcopy /y "$(VCToolsInstallDir)bin\Hostx64\x64\clang_rt.asan_dynamic-x86_64.dll" "$(OutDir)"
</Command>
Step 7: Add Preprocessor Definitions
To avoid annotation issues, add these to the Preprocessor Definitions:
_DISABLE_VECTOR_ANNOTATION;_DISABLE_STRING_ANNOTATION
Step 8: Implement the Entry Point
Every C++ fuzzing project must expose this function:
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
std::string input(reinterpret_cast<const char*>(data), size);
try
{
// Call your module with the input here
}
catch (...) {}
return 0;
}
Running Fuzzing Tests
Running Locally (.NET)
To run .NET fuzzing tests locally, follow the Running a .NET Fuzz Target Locally guide:
# Instrument the assembly
.\dotnet-fuzzing-windows\sharpfuzz\SharpFuzz.CommandLine.exe path\to\YourModule.FuzzTests.dll
# Set environment variables
$env:LIBFUZZER_DOTNET_TARGET_ASSEMBLY="path\to\YourModule.FuzzTests.dll"
$env:LIBFUZZER_DOTNET_TARGET_CLASS="YourModule.FuzzTests.FuzzTestClass"
$env:LIBFUZZER_DOTNET_TARGET_METHOD="FuzzTest"
# Run the fuzzer
.\dotnet-fuzzing-windows\libfuzzer-dotnet\libfuzzer-dotnet.exe --target_path=dotnet-fuzzing-windows\LibFuzzerDotnetLoader\LibFuzzerDotnetLoader.exe
Running in the Cloud
To submit a job to the OneFuzz cloud service, follow the OneFuzz Cloud Testing Walkthrough:
-
Run the pipeline:
- Navigate to the fuzzing pipeline
- Click "Run pipeline"
- Choose your branch and start the run
-
Alternative: Use OIP (OneFuzz Ingestion Preparation) tool:
oip submit --config .\OneFuzzConfig.json --drop-path <your_submission_directory> --platform windows --do-not-file-bugs --duration 1- Use
--do-not-file-bugsto prevent automatic bug creation during initial testing --durationspecifies the number of hours (default is 48 if not specified)
- Use
-
OneFuzz will send you an email when the job has started with a link to view results
Reviewing Results
- You'll receive an email notification when your fuzzing job starts
- Click the link in the email to view the job status on the OneFuzz Web UI
- The OneFuzz platform will show statistics like inputs processed, coverage, and any crashes found
- If the final status is "success," your fuzzing test is working correctly
Current Status
PowerToys has implemented fuzzing for several modules:
- Hosts File Editor
- Registry Preview
- Fancy Zones
Modules that still need fuzzing implementation:
- Environmental Variables
- Keyboard Manager
Requesting Access to OneFuzz
To log into the production instance of OneFuzz with the CLI, you must request access. Visit the OneFuzz Access Request Page.