[pipeline] feat: Implement flexible UI test pipeline with configurable build and execution modes (#40490)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
**Root Cause:**
The current pipeline builds the entire solution and runs all UI tests
every time, which takes more than 2 hours to complete.

**Fix**
Make the PowerToys UI test pipeline provides flexible options for
building and testing:

### Pipeline Options

- **useLatestOfficialBuild**: When checked, downloads the latest
official PowerToys build and installs it for testing. This skips the
full solution build and only builds UI test projects.

- **useCurrentBranchBuild**: When checked along with
`useLatestOfficialBuild`, downloads the official build from the current
branch instead of main.

- **uiTestModules**: Specify which UI test modules to build and run.
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

### Build Modes

1. **Official Build + Selective Testing** (`useLatestOfficialBuild =
true`)
   - Downloads and installs official PowerToys build
   - Builds only specified UI test projects
   - Runs specified UI tests against installed PowerToys
   - Controlled by `uiTestModules` parameter

2. **Full Build + Testing** (`useLatestOfficialBuild = false`)
   - Builds entire PowerToys solution
   - Builds UI test projects (all or specific based on `uiTestModules`)
   - Runs UI tests (all or specific based on `uiTestModules`)
   - Uses freshly built PowerToys for testing
  
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] **Closes:** #xxx
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
leileizhang
2025-07-10 09:26:26 +08:00
committed by GitHub
parent 608eb1e034
commit 6d29c3a2c9
10 changed files with 562 additions and 73 deletions

View File

@@ -77,7 +77,7 @@ namespace Microsoft.PowerToys.UITest
internal class ModuleConfigData
{
private Dictionary<PowerToysModule, string> ModulePath { get; }
private Dictionary<PowerToysModule, ModuleInfo> ModuleInfo { get; }
// Singleton instance of ModuleConfigData.
private static readonly Lazy<ModuleConfigData> SingletonInstance = new Lazy<ModuleConfigData>(() => new ModuleConfigData());
@@ -86,37 +86,74 @@ namespace Microsoft.PowerToys.UITest
public const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723";
public Dictionary<PowerToysModule, string> ModuleWindowName { get; }
private bool UseInstallerForTest { get; }
private ModuleConfigData()
{
// The exe window name for each module.
ModuleWindowName = new Dictionary<PowerToysModule, string>
{
[PowerToysModule.PowerToysSettings] = "PowerToys Settings",
[PowerToysModule.FancyZone] = "FancyZones Layout",
[PowerToysModule.Hosts] = "Hosts File Editor",
[PowerToysModule.Runner] = "PowerToys",
[PowerToysModule.Workspaces] = "Workspaces Editor",
[PowerToysModule.PowerRename] = "PowerRename",
};
// Check if we should use installer paths from environment variable
string? useInstallerForTestEnv =
Environment.GetEnvironmentVariable("useInstallerForTest") ?? Environment.GetEnvironmentVariable("USEINSTALLERFORTEST");
UseInstallerForTest = !string.IsNullOrEmpty(useInstallerForTestEnv) && bool.TryParse(useInstallerForTestEnv, out bool result) && result;
// Exe start path for the module if it exists.
ModulePath = new Dictionary<PowerToysModule, string>
// Module information including executable name, window name, and optional subdirectory
ModuleInfo = new Dictionary<PowerToysModule, ModuleInfo>
{
[PowerToysModule.PowerToysSettings] = @"\..\..\..\WinUI3Apps\PowerToys.Settings.exe",
[PowerToysModule.FancyZone] = @"\..\..\..\PowerToys.FancyZonesEditor.exe",
[PowerToysModule.Hosts] = @"\..\..\..\WinUI3Apps\PowerToys.Hosts.exe",
[PowerToysModule.Runner] = @"\..\..\..\PowerToys.exe",
[PowerToysModule.Workspaces] = @"\..\..\..\PowerToys.WorkspacesEditor.exe",
[PowerToysModule.PowerRename] = @"\..\..\..\WinUI3Apps\PowerToys.PowerRename.exe",
[PowerToysModule.PowerToysSettings] = new ModuleInfo("PowerToys.Settings.exe", "PowerToys Settings", "WinUI3Apps"),
[PowerToysModule.FancyZone] = new ModuleInfo("PowerToys.FancyZonesEditor.exe", "FancyZones Layout"),
[PowerToysModule.Hosts] = new ModuleInfo("PowerToys.Hosts.exe", "Hosts File Editor", "WinUI3Apps"),
[PowerToysModule.Runner] = new ModuleInfo("PowerToys.exe", "PowerToys"),
[PowerToysModule.Workspaces] = new ModuleInfo("PowerToys.WorkspacesEditor.exe", "Workspaces Editor"),
[PowerToysModule.PowerRename] = new ModuleInfo("PowerToys.PowerRename.exe", "PowerRename", "WinUI3Apps"),
};
}
public string GetModulePath(PowerToysModule scope) => ModulePath[scope];
private string GetPowerToysInstallPath()
{
// Try common installation paths
string[] possiblePaths =
{
@"C:\Program Files\PowerToys",
@"C:\Program Files (x86)\PowerToys",
Environment.ExpandEnvironmentVariables(@"%LocalAppData%\PowerToys"),
Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\PowerToys"),
};
foreach (string path in possiblePaths)
{
if (Directory.Exists(path) && File.Exists(Path.Combine(path, "PowerToys.exe")))
{
return path;
}
}
// Fallback to Program Files if not found
return @"C:\Program Files\PowerToys";
}
public string GetModulePath(PowerToysModule scope)
{
var moduleInfo = ModuleInfo[scope];
if (UseInstallerForTest)
{
string powerToysInstallPath = GetPowerToysInstallPath();
string installedPath = moduleInfo.GetInstalledPath(powerToysInstallPath);
if (File.Exists(installedPath))
{
return installedPath;
}
else
{
Console.WriteLine($"Warning: Installed module not found at {installedPath}, using development path");
}
}
return moduleInfo.GetDevelopmentPath();
}
public string GetWindowsApplicationDriverUrl() => WindowsApplicationDriverUrl;
public string GetModuleWindowName(PowerToysModule scope) => ModuleWindowName[scope];
public string GetModuleWindowName(PowerToysModule scope) => ModuleInfo[scope].WindowName;
}
}