Fix fancy zones UI tests #42249 (#44181)

- [ ] Closes: #42249

Contribution to https://github.com/microsoft/PowerToys/issues/40701
This commit is contained in:
Gleb Khmyznikov
2025-12-10 10:04:04 -08:00
committed by GitHub
parent f822826cf1
commit 995bbdc62d
12 changed files with 807 additions and 205 deletions

View File

@@ -335,3 +335,7 @@ azp
feedbackhub feedbackhub
needinfo needinfo
reportbug reportbug
#ffmpeg
crf
nostdin

View File

@@ -0,0 +1,399 @@
// 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.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Provides methods for recording the screen during UI tests.
/// Requires FFmpeg to be installed and available in PATH.
/// </summary>
internal class ScreenRecording : IDisposable
{
private readonly string outputDirectory;
private readonly string framesDirectory;
private readonly string outputFilePath;
private readonly List<string> capturedFrames;
private readonly SemaphoreSlim recordingLock = new(1, 1);
private readonly Stopwatch recordingStopwatch = new();
private readonly string? ffmpegPath;
private CancellationTokenSource? recordingCancellation;
private Task? recordingTask;
private bool isRecording;
private int frameCount;
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("gdi32.dll")]
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
[DllImport("user32.dll")]
private static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("user32.dll")]
private static extern bool GetCursorInfo(out ScreenCapture.CURSORINFO pci);
[DllImport("user32.dll")]
private static extern bool DrawIconEx(IntPtr hdc, int x, int y, IntPtr hIcon, int cx, int cy, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, int diFlags);
private const int CURSORSHOWING = 0x00000001;
private const int DESKTOPHORZRES = 118;
private const int DESKTOPVERTRES = 117;
private const int DINORMAL = 0x0003;
private const int TargetFps = 15; // 15 FPS for good balance of quality and size
/// <summary>
/// Initializes a new instance of the <see cref="ScreenRecording"/> class.
/// </summary>
/// <param name="outputDirectory">Directory where the recording will be saved.</param>
public ScreenRecording(string outputDirectory)
{
this.outputDirectory = outputDirectory;
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
framesDirectory = Path.Combine(outputDirectory, $"frames_{timestamp}");
outputFilePath = Path.Combine(outputDirectory, $"recording_{timestamp}.mp4");
capturedFrames = new List<string>();
frameCount = 0;
// Check if FFmpeg is available
ffmpegPath = FindFfmpeg();
if (ffmpegPath == null)
{
Console.WriteLine("FFmpeg not found. Screen recording will be disabled.");
Console.WriteLine("To enable video recording, install FFmpeg: https://ffmpeg.org/download.html");
}
}
/// <summary>
/// Gets a value indicating whether screen recording is available (FFmpeg found).
/// </summary>
public bool IsAvailable => ffmpegPath != null;
/// <summary>
/// Starts recording the screen.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task StartRecordingAsync()
{
await recordingLock.WaitAsync();
try
{
if (isRecording || !IsAvailable)
{
return;
}
// Create frames directory
Directory.CreateDirectory(framesDirectory);
recordingCancellation = new CancellationTokenSource();
isRecording = true;
recordingStopwatch.Start();
// Start the recording task
recordingTask = Task.Run(() => RecordFrames(recordingCancellation.Token));
Console.WriteLine($"Started screen recording at {TargetFps} FPS");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to start recording: {ex.Message}");
isRecording = false;
}
finally
{
recordingLock.Release();
}
}
/// <summary>
/// Stops recording and encodes video.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task StopRecordingAsync()
{
await recordingLock.WaitAsync();
try
{
if (!isRecording || recordingCancellation == null)
{
return;
}
// Signal cancellation
recordingCancellation.Cancel();
// Wait for recording task to complete
if (recordingTask != null)
{
await recordingTask;
}
recordingStopwatch.Stop();
isRecording = false;
double duration = recordingStopwatch.Elapsed.TotalSeconds;
Console.WriteLine($"Recording stopped. Captured {capturedFrames.Count} frames in {duration:F2} seconds");
// Encode to video
await EncodeToVideoAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Error stopping recording: {ex.Message}");
}
finally
{
Cleanup();
recordingLock.Release();
}
}
/// <summary>
/// Records frames from the screen.
/// </summary>
private void RecordFrames(CancellationToken cancellationToken)
{
try
{
int frameInterval = 1000 / TargetFps;
var frameTimer = Stopwatch.StartNew();
while (!cancellationToken.IsCancellationRequested)
{
var frameStart = frameTimer.ElapsedMilliseconds;
try
{
CaptureFrame();
}
catch (Exception ex)
{
Console.WriteLine($"Error capturing frame: {ex.Message}");
}
// Sleep for remaining time to maintain target FPS
var frameTime = frameTimer.ElapsedMilliseconds - frameStart;
var sleepTime = Math.Max(0, frameInterval - (int)frameTime);
if (sleepTime > 0)
{
Thread.Sleep(sleepTime);
}
}
}
catch (OperationCanceledException)
{
// Expected when stopping
}
catch (Exception ex)
{
Console.WriteLine($"Error during recording: {ex.Message}");
}
}
/// <summary>
/// Captures a single frame.
/// </summary>
private void CaptureFrame()
{
IntPtr hdc = GetDC(IntPtr.Zero);
int screenWidth = GetDeviceCaps(hdc, DESKTOPHORZRES);
int screenHeight = GetDeviceCaps(hdc, DESKTOPVERTRES);
ReleaseDC(IntPtr.Zero, hdc);
Rectangle bounds = new Rectangle(0, 0, screenWidth, screenHeight);
using (Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format24bppRgb))
{
using (Graphics g = Graphics.FromImage(bitmap))
{
g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);
ScreenCapture.CURSORINFO cursorInfo;
cursorInfo.CbSize = Marshal.SizeOf<ScreenCapture.CURSORINFO>();
if (GetCursorInfo(out cursorInfo) && cursorInfo.Flags == CURSORSHOWING)
{
IntPtr hdcDest = g.GetHdc();
DrawIconEx(hdcDest, cursorInfo.PTScreenPos.X, cursorInfo.PTScreenPos.Y, cursorInfo.HCursor, 0, 0, 0, IntPtr.Zero, DINORMAL);
g.ReleaseHdc(hdcDest);
}
}
string framePath = Path.Combine(framesDirectory, $"frame_{frameCount:D6}.jpg");
bitmap.Save(framePath, ImageFormat.Jpeg);
capturedFrames.Add(framePath);
frameCount++;
}
}
/// <summary>
/// Encodes captured frames to video using ffmpeg.
/// </summary>
private async Task EncodeToVideoAsync()
{
if (capturedFrames.Count == 0)
{
Console.WriteLine("No frames captured");
return;
}
try
{
// Build ffmpeg command with proper non-interactive flags
string inputPattern = Path.Combine(framesDirectory, "frame_%06d.jpg");
// -y: overwrite without asking
// -nostdin: disable interaction
// -loglevel error: only show errors
// -stats: show encoding progress
string args = $"-y -nostdin -loglevel error -stats -framerate {TargetFps} -i \"{inputPattern}\" -c:v libx264 -pix_fmt yuv420p -crf 23 \"{outputFilePath}\"";
Console.WriteLine($"Encoding {capturedFrames.Count} frames to video...");
var startInfo = new ProcessStartInfo
{
FileName = ffmpegPath!,
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true, // Important: redirect stdin to prevent hanging
CreateNoWindow = true,
};
using var process = Process.Start(startInfo);
if (process != null)
{
// Close stdin immediately to ensure FFmpeg doesn't wait for input
process.StandardInput.Close();
// Read output streams asynchronously to prevent deadlock
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
// Wait for process to exit
await process.WaitForExitAsync();
// Get the output
string stdout = await outputTask;
string stderr = await errorTask;
if (process.ExitCode == 0 && File.Exists(outputFilePath))
{
var fileInfo = new FileInfo(outputFilePath);
Console.WriteLine($"Video created: {outputFilePath} ({fileInfo.Length / 1024 / 1024:F1} MB)");
}
else
{
Console.WriteLine($"FFmpeg encoding failed with exit code {process.ExitCode}");
if (!string.IsNullOrWhiteSpace(stderr))
{
Console.WriteLine($"FFmpeg error: {stderr}");
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error encoding video: {ex.Message}");
}
}
/// <summary>
/// Finds ffmpeg executable.
/// </summary>
private static string? FindFfmpeg()
{
// Check if ffmpeg is in PATH
var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty<string>();
foreach (var dir in pathDirs)
{
var ffmpegPath = Path.Combine(dir, "ffmpeg.exe");
if (File.Exists(ffmpegPath))
{
return ffmpegPath;
}
}
// Check common installation locations
var commonPaths = new[]
{
@"C:\.tools\ffmpeg\bin\ffmpeg.exe",
@"C:\ffmpeg\bin\ffmpeg.exe",
@"C:\Program Files\ffmpeg\bin\ffmpeg.exe",
@"C:\Program Files (x86)\ffmpeg\bin\ffmpeg.exe",
@$"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\WinGet\Links\ffmpeg.exe",
};
foreach (var path in commonPaths)
{
if (File.Exists(path))
{
return path;
}
}
return null;
}
/// <summary>
/// Gets the path to the recorded video file.
/// </summary>
public string OutputFilePath => outputFilePath;
/// <summary>
/// Gets the directory containing recordings.
/// </summary>
public string OutputDirectory => outputDirectory;
/// <summary>
/// Cleans up resources.
/// </summary>
private void Cleanup()
{
recordingCancellation?.Dispose();
recordingCancellation = null;
recordingTask = null;
// Clean up frames directory if it exists
try
{
if (Directory.Exists(framesDirectory))
{
Directory.Delete(framesDirectory, true);
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to cleanup frames directory: {ex.Message}");
}
}
/// <summary>
/// Disposes resources.
/// </summary>
public void Dispose()
{
if (isRecording)
{
StopRecordingAsync().GetAwaiter().GetResult();
}
Cleanup();
recordingLock.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@@ -130,9 +130,13 @@ namespace Microsoft.PowerToys.UITest
/// </summary> /// </summary>
/// <param name="appPath">The path to the application executable.</param> /// <param name="appPath">The path to the application executable.</param>
/// <param name="args">Optional command line arguments to pass to the application.</param> /// <param name="args">Optional command line arguments to pass to the application.</param>
public void StartExe(string appPath, string[]? args = null) public void StartExe(string appPath, string[]? args = null, string? enableModules = null)
{ {
var opts = new AppiumOptions(); var opts = new AppiumOptions();
if (!string.IsNullOrEmpty(enableModules))
{
opts.AddAdditionalCapability("enableModules", enableModules);
}
if (scope == PowerToysModule.PowerToysSettings) if (scope == PowerToysModule.PowerToysSettings)
{ {
@@ -169,27 +173,66 @@ namespace Microsoft.PowerToys.UITest
private void TryLaunchPowerToysSettings(AppiumOptions opts) private void TryLaunchPowerToysSettings(AppiumOptions opts)
{ {
try if (opts.ToCapabilities().HasCapability("enableModules"))
{ {
var runnerProcessInfo = new ProcessStartInfo var modulesString = (string)opts.ToCapabilities().GetCapability("enableModules");
var modulesArray = modulesString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
SettingsConfigHelper.ConfigureGlobalModuleSettings(modulesArray);
}
else
{
SettingsConfigHelper.ConfigureGlobalModuleSettings();
}
const int maxTries = 3;
const int delayMs = 5000;
const int maxRetries = 3;
for (int tryCount = 1; tryCount <= maxTries; tryCount++)
{
try
{ {
FileName = locationPath + runnerPath, var runnerProcessInfo = new ProcessStartInfo
Verb = "runas", {
Arguments = "--open-settings", FileName = locationPath + runnerPath,
}; Verb = "runas",
Arguments = "--open-settings",
};
ExitExe(runnerProcessInfo.FileName); ExitExe(runnerProcessInfo.FileName);
runner = Process.Start(runnerProcessInfo);
WaitForWindowAndSetCapability(opts, "PowerToys Settings", 5000, 5); // Verify process was killed
string exeName = Path.GetFileNameWithoutExtension(runnerProcessInfo.FileName);
var remainingProcesses = Process.GetProcessesByName(exeName);
// Exit CmdPal UI before launching new process if use installer for test runner = Process.Start(runnerProcessInfo);
ExitExeByName("Microsoft.CmdPal.UI");
} if (WaitForWindowAndSetCapability(opts, "PowerToys Settings", delayMs, maxRetries))
catch (Exception ex) {
{ // Exit CmdPal UI before launching new process if use installer for test
throw new InvalidOperationException($"Failed to launch PowerToys Settings: {ex.Message}", ex); ExitExeByName("Microsoft.CmdPal.UI");
return;
}
// Window not found, kill all PowerToys processes and retry
if (tryCount < maxTries)
{
KillPowerToysProcesses();
}
}
catch (Exception ex)
{
if (tryCount == maxTries)
{
throw new InvalidOperationException($"Failed to launch PowerToys Settings after {maxTries} attempts: {ex.Message}", ex);
}
// Kill processes and retry
KillPowerToysProcesses();
}
} }
throw new InvalidOperationException($"Failed to launch PowerToys Settings: Window not found after {maxTries} attempts.");
} }
private void TryLaunchCommandPalette(AppiumOptions opts) private void TryLaunchCommandPalette(AppiumOptions opts)
@@ -211,7 +254,10 @@ namespace Microsoft.PowerToys.UITest
var process = Process.Start(processStartInfo); var process = Process.Start(processStartInfo);
process?.WaitForExit(); process?.WaitForExit();
WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10); if (!WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10))
{
throw new TimeoutException("Failed to find Command Palette window after multiple attempts.");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -219,7 +265,7 @@ namespace Microsoft.PowerToys.UITest
} }
} }
private void WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries) private bool WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries)
{ {
for (int attempt = 1; attempt <= maxRetries; attempt++) for (int attempt = 1; attempt <= maxRetries; attempt++)
{ {
@@ -230,18 +276,16 @@ namespace Microsoft.PowerToys.UITest
{ {
var hexHwnd = window[0].HWnd.ToString("x"); var hexHwnd = window[0].HWnd.ToString("x");
opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd); opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd);
return; return true;
} }
if (attempt < maxRetries) if (attempt < maxRetries)
{ {
Thread.Sleep(delayMs); Thread.Sleep(delayMs);
} }
else
{
throw new TimeoutException($"Failed to find {windowName} window after multiple attempts.");
}
} }
return false;
} }
/// <summary> /// <summary>
@@ -292,17 +336,17 @@ namespace Microsoft.PowerToys.UITest
catch (Exception ex) catch (Exception ex)
{ {
// Handle exceptions if needed // Handle exceptions if needed
Debug.WriteLine($"Exception during Cleanup: {ex.Message}"); Console.WriteLine($"Exception during Cleanup: {ex.Message}");
} }
} }
/// <summary> /// <summary>
/// Restarts now exe and takes control of it. /// Restarts now exe and takes control of it.
/// </summary> /// </summary>
public void RestartScopeExe() public void RestartScopeExe(string? enableModules = null)
{ {
ExitScopeExe(); ExitScopeExe();
StartExe(locationPath + sessionPath, this.commandLineArgs); StartExe(locationPath + sessionPath, commandLineArgs, enableModules);
} }
public WindowsDriver<WindowsElement> GetRoot() public WindowsDriver<WindowsElement> GetRoot()
@@ -327,5 +371,31 @@ namespace Microsoft.PowerToys.UITest
this.ExitExe(winAppDriverProcessInfo.FileName); this.ExitExe(winAppDriverProcessInfo.FileName);
SessionHelper.appDriver = Process.Start(winAppDriverProcessInfo); SessionHelper.appDriver = Process.Start(winAppDriverProcessInfo);
} }
private void KillPowerToysProcesses()
{
var powerToysProcessNames = new[] { "PowerToys", "Microsoft.CmdPal.UI" };
foreach (var processName in powerToysProcessNames)
{
try
{
var processes = Process.GetProcessesByName(processName);
foreach (var process in processes)
{
process.Kill();
process.WaitForExit();
}
// Verify processes are actually gone
var remainingProcesses = Process.GetProcessesByName(processName);
}
catch (Exception ex)
{
Console.WriteLine($"[KillPowerToysProcesses] Failed to kill process {processName}: {ex.Message}");
}
}
}
} }
} }

View File

@@ -26,14 +26,13 @@ namespace Microsoft.PowerToys.UITest
/// <summary> /// <summary>
/// Configures global PowerToys settings to enable only specified modules and disable all others. /// Configures global PowerToys settings to enable only specified modules and disable all others.
/// </summary> /// </summary>
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled.</param> /// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled. If null or empty, all modules will be disabled.</param>
/// <exception cref="ArgumentNullException">Thrown when modulesToEnable is null.</exception>
/// <exception cref="InvalidOperationException">Thrown when settings file operations fail.</exception> /// <exception cref="InvalidOperationException">Thrown when settings file operations fail.</exception>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")] [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")]
public static void ConfigureGlobalModuleSettings(params string[] modulesToEnable) public static void ConfigureGlobalModuleSettings(params string[]? modulesToEnable)
{ {
ArgumentNullException.ThrowIfNull(modulesToEnable); modulesToEnable ??= Array.Empty<string>();
try try
{ {

View File

@@ -29,6 +29,8 @@ namespace Microsoft.PowerToys.UITest
public string? ScreenshotDirectory { get; set; } public string? ScreenshotDirectory { get; set; }
public string? RecordingDirectory { get; set; }
public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List<MonitorInfoData.MonitorInfoDataWrapper>() }; public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List<MonitorInfoData.MonitorInfoDataWrapper>() };
private readonly PowerToysModule scope; private readonly PowerToysModule scope;
@@ -36,6 +38,7 @@ namespace Microsoft.PowerToys.UITest
private readonly string[]? commandLineArgs; private readonly string[]? commandLineArgs;
private SessionHelper? sessionHelper; private SessionHelper? sessionHelper;
private System.Threading.Timer? screenshotTimer; private System.Threading.Timer? screenshotTimer;
private ScreenRecording? screenRecording;
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified, string[]? commandLineArgs = null) public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified, string[]? commandLineArgs = null)
{ {
@@ -65,12 +68,35 @@ namespace Microsoft.PowerToys.UITest
CloseOtherApplications(); CloseOtherApplications();
if (IsInPipeline) if (IsInPipeline)
{ {
ScreenshotDirectory = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "UITestScreenshots_" + Guid.NewGuid().ToString()); string baseDirectory = this.TestContext.TestResultsDirectory ?? string.Empty;
ScreenshotDirectory = Path.Combine(baseDirectory, "UITestScreenshots_" + Guid.NewGuid().ToString());
Directory.CreateDirectory(ScreenshotDirectory); Directory.CreateDirectory(ScreenshotDirectory);
RecordingDirectory = Path.Combine(baseDirectory, "UITestRecordings_" + Guid.NewGuid().ToString());
Directory.CreateDirectory(RecordingDirectory);
// Take screenshot every 1 second // Take screenshot every 1 second
screenshotTimer = new System.Threading.Timer(ScreenCapture.TimerCallback, ScreenshotDirectory, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000)); screenshotTimer = new System.Threading.Timer(ScreenCapture.TimerCallback, ScreenshotDirectory, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000));
// Start screen recording (requires FFmpeg)
try
{
screenRecording = new ScreenRecording(RecordingDirectory);
if (screenRecording.IsAvailable)
{
_ = screenRecording.StartRecordingAsync();
}
else
{
screenRecording = null;
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to start screen recording: {ex.Message}");
screenRecording = null;
}
// Escape Popups before starting // Escape Popups before starting
System.Windows.Forms.SendKeys.SendWait("{ESC}"); System.Windows.Forms.SendKeys.SendWait("{ESC}");
} }
@@ -88,15 +114,36 @@ namespace Microsoft.PowerToys.UITest
if (IsInPipeline) if (IsInPipeline)
{ {
screenshotTimer?.Change(Timeout.Infinite, Timeout.Infinite); screenshotTimer?.Change(Timeout.Infinite, Timeout.Infinite);
Dispose();
// Stop screen recording
if (screenRecording != null)
{
try
{
screenRecording.StopRecordingAsync().GetAwaiter().GetResult();
}
catch (Exception ex)
{
Console.WriteLine($"Failed to stop screen recording: {ex.Message}");
}
}
if (TestContext.CurrentTestOutcome is UnitTestOutcome.Failed if (TestContext.CurrentTestOutcome is UnitTestOutcome.Failed
or UnitTestOutcome.Error or UnitTestOutcome.Error
or UnitTestOutcome.Unknown) or UnitTestOutcome.Unknown)
{ {
Task.Delay(1000).Wait(); Task.Delay(1000).Wait();
AddScreenShotsToTestResultsDirectory(); AddScreenShotsToTestResultsDirectory();
AddRecordingsToTestResultsDirectory();
AddLogFilesToTestResultsDirectory(); AddLogFilesToTestResultsDirectory();
} }
else
{
// Clean up recording if test passed
CleanupRecordingDirectory();
}
Dispose();
} }
this.Session.Cleanup(); this.Session.Cleanup();
@@ -106,6 +153,7 @@ namespace Microsoft.PowerToys.UITest
public void Dispose() public void Dispose()
{ {
screenshotTimer?.Dispose(); screenshotTimer?.Dispose();
screenRecording?.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
@@ -600,6 +648,47 @@ namespace Microsoft.PowerToys.UITest
} }
} }
/// <summary>
/// Adds screen recordings to test results directory when test fails.
/// </summary>
protected void AddRecordingsToTestResultsDirectory()
{
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
{
// Add video files (MP4)
var videoFiles = Directory.GetFiles(RecordingDirectory, "*.mp4");
foreach (string file in videoFiles)
{
this.TestContext.AddResultFile(file);
var fileInfo = new FileInfo(file);
Console.WriteLine($"Added video recording: {Path.GetFileName(file)} ({fileInfo.Length / 1024 / 1024:F1} MB)");
}
if (videoFiles.Length == 0)
{
Console.WriteLine("No video recording available (FFmpeg not found). Screenshots are still captured.");
}
}
}
/// <summary>
/// Cleans up recording directory when test passes.
/// </summary>
private void CleanupRecordingDirectory()
{
if (RecordingDirectory != null && Directory.Exists(RecordingDirectory))
{
try
{
Directory.Delete(RecordingDirectory, true);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to cleanup recording directory: {ex.Message}");
}
}
}
/// <summary> /// <summary>
/// Copies PowerToys log files to test results directory when test fails. /// Copies PowerToys log files to test results directory when test fails.
/// Renames files to include the directory structure after \PowerToys. /// Renames files to include the directory structure after \PowerToys.
@@ -689,11 +778,11 @@ namespace Microsoft.PowerToys.UITest
/// <summary> /// <summary>
/// Restart scope exe. /// Restart scope exe.
/// </summary> /// </summary>
public void RestartScopeExe() public Session RestartScopeExe(string? enableModules = null)
{ {
this.sessionHelper!.RestartScopeExe(); this.sessionHelper!.RestartScopeExe(enableModules);
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), this.scope, this.size); this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), this.scope, this.size);
return; return Session;
} }
/// <summary> /// <summary>

View File

@@ -617,6 +617,8 @@ namespace MouseUtils.UITests
private void LaunchFromSetting(bool reload = false, bool launchAsAdmin = false) private void LaunchFromSetting(bool reload = false, bool launchAsAdmin = false)
{ {
Session = RestartScopeExe("FindMyMouse,MouseHighlighter,MouseJump,MousePointerCrosshairs,CursorWrap");
// this.Session.Attach(PowerToysModule.PowerToysSettings); // this.Session.Attach(PowerToysModule.PowerToysSettings);
this.Session.SetMainWindowSize(WindowSize.Large); this.Session.SetMainWindowSize(WindowSize.Large);

View File

@@ -105,6 +105,7 @@ public sealed partial class SettingsWindow : WindowEx,
"Extensions" => typeof(ExtensionsPage), "Extensions" => typeof(ExtensionsPage),
_ => null, _ => null,
}; };
if (pageType is not null) if (pageType is not null)
{ {
NavFrame.Navigate(pageType); NavFrame.Navigate(pageType);

View File

@@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using FancyZonesEditor.Models; using FancyZonesEditor.Models;
@@ -49,19 +50,16 @@ namespace UITests_FancyZones
[TestInitialize] [TestInitialize]
public void TestInitialize() public void TestInitialize()
{ {
// ClearOpenWindows Session.KillAllProcessesByName("PowerToys");
ClearOpenWindows(); ClearOpenWindows();
// kill all processes related to FancyZones Editor to ensure a clean state
Session.KillAllProcessesByName("PowerToys.FancyZonesEditor");
AppZoneHistory.DeleteFile(); AppZoneHistory.DeleteFile();
this.RestartScopeExe();
FancyZonesEditorHelper.Files.Restore(); FancyZonesEditorHelper.Files.Restore();
// Set a custom layout with 1 subzones and clear app zone history
SetupCustomLayouts(); SetupCustomLayouts();
RestartScopeExe("Hosts");
Thread.Sleep(2000);
// Get the current mouse button setting // Get the current mouse button setting
nonPrimaryMouseButton = SystemInformation.MouseButtonsSwapped ? "Left" : "Right"; nonPrimaryMouseButton = SystemInformation.MouseButtonsSwapped ? "Left" : "Right";
@@ -72,99 +70,6 @@ namespace UITests_FancyZones
LaunchFancyZones(); LaunchFancyZones();
} }
/// <summary>
/// Test Use Shift key to activate zones while dragging a window in FancyZones Zone Behaviour Settings
/// <list type="bullet">
/// <item>
/// <description>Verifies that holding Shift while dragging shows all zones as expected.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestShowZonesOnShiftDuringDrag")]
[TestCategory("FancyZones_Dragging #1")]
public void TestShowZonesOnShiftDuringDrag()
{
string testCaseName = nameof(TestShowZonesOnShiftDuringDrag);
Pane dragElement = Find<Pane>(By.Name("Non Client Input Sink Window")); // element to drag
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY);
var (initialColor, withShiftColor) = RunDragInteractions(
preAction: () =>
{
dragElement.DragAndHold(offSet.Dx, offSet.Dy);
},
postAction: () =>
{
Session.PressKey(Key.Shift);
Task.Delay(500).Wait();
},
releaseAction: () =>
{
Session.ReleaseKey(Key.Shift);
Task.Delay(5000).Wait(); // Optional: Wait for a moment to ensure window switch
},
testCaseName: testCaseName);
string zoneColorWithoutShift = GetOutWindowPixelColor(30);
Assert.AreNotEqual(initialColor, withShiftColor, $"[{testCaseName}] Zone display failed.");
Assert.IsTrue(
withShiftColor == inactivateColor || withShiftColor == highlightColor,
$"[{testCaseName}] Zone display failed: withShiftColor was {withShiftColor}, expected {inactivateColor} or {highlightColor}.");
Assert.AreEqual(inactivateColor, withShiftColor, $"[{testCaseName}] Zone display failed.");
Assert.AreEqual(zoneColorWithoutShift, initialColor, $"[{testCaseName}] Zone deactivated failed.");
dragElement.ReleaseDrag();
Clean();
}
/// <summary>
/// Test dragging a window during Shift key press in FancyZones Zone Behaviour Settings
/// <list type="bullet">
/// <item>
/// <description>Verifies that dragging activates zones as expected.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestShowZonesOnDragDuringShift")]
[TestCategory("FancyZones_Dragging #2")]
public void TestShowZonesOnDragDuringShift()
{
string testCaseName = nameof(TestShowZonesOnDragDuringShift);
var dragElement = Find<Pane>(By.Name("Non Client Input Sink Window"));
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY);
var (initialColor, withDragColor) = RunDragInteractions(
preAction: () =>
{
dragElement.Drag(offSet.Dx, offSet.Dy);
Session.PressKey(Key.Shift);
},
postAction: () =>
{
dragElement.DragAndHold(0, 0);
Task.Delay(5000).Wait();
},
releaseAction: () =>
{
dragElement.ReleaseDrag();
Session.ReleaseKey(Key.Shift);
},
testCaseName: testCaseName);
Assert.AreNotEqual(initialColor, withDragColor, $"[{testCaseName}] Zone color did not change; zone activation failed.");
Assert.AreEqual(highlightColor, withDragColor, $"[{testCaseName}] Zone color did not match the highlight color; activation failed.");
// double check by app-zone-history.json
string appZoneHistoryJson = AppZoneHistory.GetData();
string? zoneNumber = ZoneSwitchHelper.GetZoneIndexSetByAppName(powertoysWindowName, appZoneHistoryJson);
Assert.IsNull(zoneNumber, $"[{testCaseName}] AppZoneHistory layout was unexpectedly set.");
Clean();
}
/// <summary> /// <summary>
/// Test toggling zones using a non-primary mouse click during window dragging. /// Test toggling zones using a non-primary mouse click during window dragging.
/// <list type="bullet"> /// <list type="bullet">
@@ -178,14 +83,19 @@ namespace UITests_FancyZones
public void TestToggleZonesWithNonPrimaryMouseClick() public void TestToggleZonesWithNonPrimaryMouseClick()
{ {
string testCaseName = nameof(TestToggleZonesWithNonPrimaryMouseClick); string testCaseName = nameof(TestToggleZonesWithNonPrimaryMouseClick);
var dragElement = Find<Pane>(By.Name("Non Client Input Sink Window"));
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY); var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
var (initialColor, withMouseColor) = RunDragInteractions( var (initialColor, withMouseColor) = RunDragInteractions(
preAction: () => preAction: () =>
{ {
// activate zone Session.MoveMouseTo(startX, startY);
dragElement.DragAndHold(offSet.Dx, offSet.Dy); Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
}, },
postAction: () => postAction: () =>
{ {
@@ -195,7 +105,7 @@ namespace UITests_FancyZones
}, },
releaseAction: () => releaseAction: () =>
{ {
dragElement.ReleaseDrag(); Session.PerformMouseAction(MouseActionType.LeftUp);
}, },
testCaseName: testCaseName); testCaseName: testCaseName);
@@ -204,8 +114,6 @@ namespace UITests_FancyZones
// check the zone color is activated // check the zone color is activated
Assert.AreEqual(highlightColor, initialColor, $"[{testCaseName}] Zone activation failed."); Assert.AreEqual(highlightColor, initialColor, $"[{testCaseName}] Zone activation failed.");
Clean();
} }
/// <summary> /// <summary>
@@ -221,32 +129,35 @@ namespace UITests_FancyZones
public void TestShowZonesWhenShiftAndMouseOff() public void TestShowZonesWhenShiftAndMouseOff()
{ {
string testCaseName = nameof(TestShowZonesWhenShiftAndMouseOff); string testCaseName = nameof(TestShowZonesWhenShiftAndMouseOff);
Pane dragElement = Find<Pane>(By.Name("Non Client Input Sink Window"));
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY); var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
var (initialColor, withShiftColor) = RunDragInteractions( var (initialColor, withShiftColor) = RunDragInteractions(
preAction: () => preAction: () =>
{ {
// activate zone Session.MoveMouseTo(startX, startY);
dragElement.DragAndHold(offSet.Dx, offSet.Dy); Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
}, },
postAction: () => postAction: () =>
{ {
// press Shift Key to deactivate zones // press Shift Key to deactivate zones
Session.PressKey(Key.Shift); Session.PressKey(Key.Shift);
Task.Delay(500).Wait(); Task.Delay(1000).Wait();
}, },
releaseAction: () => releaseAction: () =>
{ {
dragElement.ReleaseDrag(); Session.PerformMouseAction(MouseActionType.LeftUp);
Session.ReleaseKey(Key.Shift); Session.ReleaseKey(Key.Shift);
}, },
testCaseName: testCaseName); testCaseName: testCaseName);
Assert.AreEqual(highlightColor, initialColor, $"[{testCaseName}] Zone activation failed."); Assert.AreEqual(highlightColor, initialColor, $"[{testCaseName}] Zone activation failed.");
Assert.AreNotEqual(highlightColor, withShiftColor, $"[{testCaseName}] Zone deactivation failed."); Assert.AreNotEqual(highlightColor, withShiftColor, $"[{testCaseName}] Zone deactivation failed.");
Clean();
} }
/// <summary> /// <summary>
@@ -263,12 +174,17 @@ namespace UITests_FancyZones
{ {
string testCaseName = nameof(TestShowZonesWhenShiftAndMouseOn); string testCaseName = nameof(TestShowZonesWhenShiftAndMouseOn);
var dragElement = Find<Pane>(By.Name("Non Client Input Sink Window")); var windowRect = Session.GetMainWindowRect();
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY); int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
var (initialColor, withShiftColor) = RunDragInteractions( var (initialColor, withShiftColor) = RunDragInteractions(
preAction: () => preAction: () =>
{ {
dragElement.DragAndHold(offSet.Dx, offSet.Dy); Session.MoveMouseTo(startX, startY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
}, },
postAction: () => postAction: () =>
{ {
@@ -279,7 +195,7 @@ namespace UITests_FancyZones
}, },
testCaseName: testCaseName); testCaseName: testCaseName);
Assert.AreEqual(inactivateColor, withShiftColor, $"[{testCaseName}] show zone failed."); Assert.AreEqual(highlightColor, withShiftColor, $"[{testCaseName}] show zone failed.");
Session.PerformMouseAction( Session.PerformMouseAction(
nonPrimaryMouseButton == "Right" ? MouseActionType.RightClick : MouseActionType.LeftClick); nonPrimaryMouseButton == "Right" ? MouseActionType.RightClick : MouseActionType.LeftClick);
@@ -288,9 +204,7 @@ namespace UITests_FancyZones
Assert.AreEqual(initialColor, zoneColorWithMouse, $"[{nameof(TestShowZonesWhenShiftAndMouseOff)}] Zone deactivate failed."); Assert.AreEqual(initialColor, zoneColorWithMouse, $"[{nameof(TestShowZonesWhenShiftAndMouseOff)}] Zone deactivate failed.");
Session.ReleaseKey(Key.Shift); Session.ReleaseKey(Key.Shift);
dragElement.ReleaseDrag(); Session.PerformMouseAction(MouseActionType.LeftUp);
Clean();
} }
/// <summary> /// <summary>
@@ -307,8 +221,6 @@ namespace UITests_FancyZones
{ {
var pixel = GetPixelWhenMakeDraggedWindow(); var pixel = GetPixelWhenMakeDraggedWindow();
Assert.AreNotEqual(pixel.PixelInWindow, pixel.TransPixel, $"[{nameof(TestMakeDraggedWindowTransparentOn)}] Window transparency failed."); Assert.AreNotEqual(pixel.PixelInWindow, pixel.TransPixel, $"[{nameof(TestMakeDraggedWindowTransparentOn)}] Window transparency failed.");
Clean();
} }
/// <summary> /// <summary>
@@ -325,14 +237,103 @@ namespace UITests_FancyZones
{ {
var pixel = GetPixelWhenMakeDraggedWindow(); var pixel = GetPixelWhenMakeDraggedWindow();
Assert.AreEqual(pixel.PixelInWindow, pixel.TransPixel, $"[{nameof(TestMakeDraggedWindowTransparentOff)}] Window without transparency failed."); Assert.AreEqual(pixel.PixelInWindow, pixel.TransPixel, $"[{nameof(TestMakeDraggedWindowTransparentOff)}] Window without transparency failed.");
Clean();
} }
private void Clean() /// <summary>
/// Test Use Shift key to activate zones while dragging a window in FancyZones Zone Behaviour Settings
/// <list type="bullet">
/// <item>
/// <description>Verifies that holding Shift while dragging shows all zones as expected.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestShowZonesOnShiftDuringDrag")]
[TestCategory("FancyZones_Dragging #1")]
public void TestShowZonesOnShiftDuringDrag()
{ {
// clean app zone history file string testCaseName = nameof(TestShowZonesOnShiftDuringDrag);
AppZoneHistory.DeleteFile();
var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
var (initialColor, withShiftColor) = RunDragInteractions(
preAction: () =>
{
Session.MoveMouseTo(startX, startY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
},
postAction: () =>
{
Session.PressKey(Key.Shift);
Task.Delay(500).Wait();
},
releaseAction: () =>
{
Session.ReleaseKey(Key.Shift);
Task.Delay(1000).Wait(); // Optional: Wait for a moment to ensure window switch
},
testCaseName: testCaseName);
string zoneColorWithoutShift = GetOutWindowPixelColor(30);
Assert.AreNotEqual(initialColor, withShiftColor, $"[{testCaseName}] Zone color did not change; zone activation failed.");
Assert.AreEqual(highlightColor, withShiftColor, $"[{testCaseName}] Zone color did not match the highlight color; activation failed.");
Session.PerformMouseAction(MouseActionType.LeftUp);
}
/// <summary>
/// Test dragging a window during Shift key press in FancyZones Zone Behaviour Settings
/// <list type="bullet">
/// <item>
/// <description>Verifies that dragging activates zones as expected.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("FancyZones.Settings.TestShowZonesOnDragDuringShift")]
[TestCategory("FancyZones_Dragging #2")]
public void TestShowZonesOnDragDuringShift()
{
string testCaseName = nameof(TestShowZonesOnDragDuringShift);
var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 300;
int endY = startY + 300;
var (initialColor, withDragColor) = RunDragInteractions(
preAction: () =>
{
Session.PressKey(Key.Shift);
Task.Delay(100).Wait();
},
postAction: () =>
{
Session.MoveMouseTo(startX, startY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.MoveMouseTo(endX, endY);
Task.Delay(1000).Wait();
},
releaseAction: () =>
{
Session.PerformMouseAction(MouseActionType.LeftUp);
Session.ReleaseKey(Key.Shift);
Task.Delay(100).Wait();
},
testCaseName: testCaseName);
Assert.AreNotEqual(initialColor, withDragColor, $"[{testCaseName}] Zone color did not change; zone activation failed.");
Assert.AreEqual(highlightColor, withDragColor, $"[{testCaseName}] Zone color did not match the highlight color; activation failed.");
// double check by app-zone-history.json
string appZoneHistoryJson = AppZoneHistory.GetData();
string? zoneNumber = ZoneSwitchHelper.GetZoneIndexSetByAppName(powertoysWindowName, appZoneHistoryJson);
Assert.IsNull(zoneNumber, $"[{testCaseName}] AppZoneHistory layout was unexpectedly set.");
} }
// Helper method to ensure the desktop has no open windows by clicking the "Show Desktop" button // Helper method to ensure the desktop has no open windows by clicking the "Show Desktop" button
@@ -352,7 +353,7 @@ namespace UITests_FancyZones
desktopButtonName = "Show Desktop"; desktopButtonName = "Show Desktop";
} }
this.Find<Microsoft.PowerToys.UITest.Button>(By.Name(desktopButtonName), 5000, true).Click(false, 500, 2000); this.Find<Microsoft.PowerToys.UITest.Button>(By.Name(desktopButtonName), 5000, true).Click(false, 500, 1000);
} }
// Setup custom layout with 1 subzones // Setup custom layout with 1 subzones
@@ -382,6 +383,11 @@ namespace UITests_FancyZones
this.Scroll(6, "Down"); // Pull the settings page up to make sure the settings are visible this.Scroll(6, "Down"); // Pull the settings page up to make sure the settings are visible
ZoneBehaviourSettings(TestContext.TestName); ZoneBehaviourSettings(TestContext.TestName);
// Go back and forth to make sure settings applied
this.Find<NavigationViewItem>("Workspaces").Click();
Task.Delay(200).Wait();
this.Find<NavigationViewItem>("FancyZones").Click();
this.Find<Microsoft.PowerToys.UITest.Button>(By.AccessibilityId("LaunchLayoutEditorButton")).Click(false, 500, 10000); this.Find<Microsoft.PowerToys.UITest.Button>(By.AccessibilityId("LaunchLayoutEditorButton")).Click(false, 500, 10000);
this.Session.Attach(PowerToysModule.FancyZone); this.Session.Attach(PowerToysModule.FancyZone);
@@ -435,22 +441,26 @@ namespace UITests_FancyZones
// Get the mouse color of the pixel when make dragged window // Get the mouse color of the pixel when make dragged window
private (string PixelInWindow, string TransPixel) GetPixelWhenMakeDraggedWindow() private (string PixelInWindow, string TransPixel) GetPixelWhenMakeDraggedWindow()
{ {
var dragElement = Find<Pane>(By.Name("Non Client Input Sink Window")); var windowRect = Session.GetMainWindowRect();
int startX = windowRect.Left + 70;
int startY = windowRect.Top + 25;
int endX = startX + 100;
int endY = startY + 100;
// maximize the window to make sure get pixel color more accurate Session.MoveMouseTo(startX, startY);
dragElement.DoubleClick();
var offSet = ZoneSwitchHelper.GetOffset(dragElement, quarterX, quarterY); // Session.PerformMouseAction(MouseActionType.LeftDoubleClick);
Session.PressKey(Key.Shift); Session.PressKey(Key.Shift);
dragElement.DragAndHold(offSet.Dx, offSet.Dy); Session.PerformMouseAction(MouseActionType.LeftDown);
Task.Delay(1000).Wait(); // Optional: Wait for a moment to ensure the window is in position Session.MoveMouseTo(endX, endY);
Tuple<int, int> pos = GetMousePosition(); Tuple<int, int> pos = GetMousePosition();
string pixelInWindow = this.GetPixelColorString(pos.Item1, pos.Item2); string pixelInWindow = this.GetPixelColorString(pos.Item1, pos.Item2);
Session.ReleaseKey(Key.Shift); Session.ReleaseKey(Key.Shift);
Task.Delay(1000).Wait(); // Optional: Wait for a moment to ensure the window is in position Task.Delay(1000).Wait();
string transPixel = this.GetPixelColorString(pos.Item1, pos.Item2); string transPixel = this.GetPixelColorString(pos.Item1, pos.Item2);
dragElement.ReleaseDrag();
Session.PerformMouseAction(MouseActionType.LeftUp);
return (pixelInWindow, transPixel); return (pixelInWindow, transPixel);
} }

View File

@@ -271,7 +271,7 @@ namespace UITests_FancyZones
}; };
FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper)); FancyZonesEditorHelper.Files.AppliedLayoutsIOHelper.WriteData(appliedLayouts.Serialize(appliedLayoutsWrapper));
this.RestartScopeExe(); RestartScopeExe("Hosts");
} }
[TestMethod("FancyZones.Settings.TestApplyHotKey")] [TestMethod("FancyZones.Settings.TestApplyHotKey")]
@@ -598,10 +598,12 @@ namespace UITests_FancyZones
this.TryReaction(); this.TryReaction();
int tries = 24; int tries = 24;
Pull(tries, "down"); // Pull the setting page up to make sure the setting is visible Pull(tries, "down"); // Pull the setting page up to make sure the setting is visible
this.Find<ToggleSwitch>("Enable quick layout switch").Toggle(flag); this.Find<ToggleSwitch>("FancyZonesQuickLayoutSwitch").Toggle(flag);
tries = 24; // Go back and forth to make sure settings applied
Pull(tries, "up"); this.Find<NavigationViewItem>("Workspaces").Click();
Task.Delay(200).Wait();
this.Find<NavigationViewItem>("FancyZones").Click();
} }
private void TryReaction() private void TryReaction()

View File

@@ -34,7 +34,7 @@ namespace UITests_FancyZones
Session.KillAllProcessesByName("PowerToys.FancyZonesEditor"); Session.KillAllProcessesByName("PowerToys.FancyZonesEditor");
AppZoneHistory.DeleteFile(); AppZoneHistory.DeleteFile();
this.RestartScopeExe(); RestartScopeExe("Hosts");
FancyZonesEditorHelper.Files.Restore(); FancyZonesEditorHelper.Files.Restore();
// Set a custom layout with 1 subzones and clear app zone history // Set a custom layout with 1 subzones and clear app zone history
@@ -137,7 +137,7 @@ namespace UITests_FancyZones
Task.Delay(500).Wait(); // Optional: Wait for a moment to ensure window switch Task.Delay(500).Wait(); // Optional: Wait for a moment to ensure window switch
activeWindowTitle = ZoneSwitchHelper.GetActiveWindowTitle(); activeWindowTitle = ZoneSwitchHelper.GetActiveWindowTitle();
Assert.AreNotEqual(preWindow, activeWindowTitle); Assert.AreEqual(postWindow, activeWindowTitle);
Clean(); // close the windows Clean(); // close the windows
} }
@@ -151,9 +151,23 @@ namespace UITests_FancyZones
var rect = Session.GetMainWindowRect(); var rect = Session.GetMainWindowRect();
var (targetX, targetY) = ZoneSwitchHelper.GetScreenMargins(rect, 4); var (targetX, targetY) = ZoneSwitchHelper.GetScreenMargins(rect, 4);
var offSet = ZoneSwitchHelper.GetOffset(hostsView, targetX, targetY);
DragWithShift(hostsView, offSet); // Snap first window (Hosts) to left zone using shift+drag with direct mouse movement
var hostsRect = hostsView.Rect ?? throw new InvalidOperationException("Failed to get hosts window rect");
int hostsStartX = hostsRect.Left + 70;
int hostsStartY = hostsRect.Top + 25;
// For a 2-column layout, left zone is at approximately 1/4 of screen width
int hostsEndX = rect.Left + (3 * (rect.Right - rect.Left) / 4);
int hostsEndY = rect.Top + ((rect.Bottom - rect.Top) / 2);
Session.MoveMouseTo(hostsStartX, hostsStartY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.PressKey(Key.Shift);
Session.MoveMouseTo(hostsEndX, hostsEndY);
Session.PerformMouseAction(MouseActionType.LeftUp);
Session.ReleaseKey(Key.Shift);
Task.Delay(500).Wait(); // Wait for snap to complete
string preWindow = ZoneSwitchHelper.GetActiveWindowTitle(); string preWindow = ZoneSwitchHelper.GetActiveWindowTitle();
@@ -163,11 +177,26 @@ namespace UITests_FancyZones
Pane settingsView = Find<Pane>(By.Name("Non Client Input Sink Window")); Pane settingsView = Find<Pane>(By.Name("Non Client Input Sink Window"));
settingsView.DoubleClick(); // maximize the window settingsView.DoubleClick(); // maximize the window
DragWithShift(settingsView, offSet); var windowRect = Session.GetMainWindowRect();
var settingsRect = settingsView.Rect ?? throw new InvalidOperationException("Failed to get settings window rect");
int settingsStartX = settingsRect.Left + 70;
int settingsStartY = settingsRect.Top + 25;
// For a 2-column layout, right zone is at approximately 3/4 of screen width
int settingsEndX = windowRect.Left + (3 * (windowRect.Right - windowRect.Left) / 4);
int settingsEndY = windowRect.Top + ((windowRect.Bottom - windowRect.Top) / 2);
Session.MoveMouseTo(settingsStartX, settingsStartY);
Session.PerformMouseAction(MouseActionType.LeftDown);
Session.PressKey(Key.Shift);
Session.MoveMouseTo(settingsEndX, settingsEndY);
Session.PerformMouseAction(MouseActionType.LeftUp);
Session.ReleaseKey(Key.Shift);
Task.Delay(500).Wait(); // Wait for snap to complete
string appZoneHistoryJson = AppZoneHistory.GetData(); string appZoneHistoryJson = AppZoneHistory.GetData();
string? zoneIndexOfFileWindow = ZoneSwitchHelper.GetZoneIndexSetByAppName("PowerToys.Hosts.exe", appZoneHistoryJson); // explorer.exe string? zoneIndexOfFileWindow = ZoneSwitchHelper.GetZoneIndexSetByAppName("PowerToys.Hosts.exe", appZoneHistoryJson);
string? zoneIndexOfPowertoys = ZoneSwitchHelper.GetZoneIndexSetByAppName("PowerToys.Settings.exe", appZoneHistoryJson); string? zoneIndexOfPowertoys = ZoneSwitchHelper.GetZoneIndexSetByAppName("PowerToys.Settings.exe", appZoneHistoryJson);
// check the AppZoneHistory layout is set and in the same zone // check the AppZoneHistory layout is set and in the same zone
@@ -176,16 +205,6 @@ namespace UITests_FancyZones
return (preWindow, powertoysWindowName); return (preWindow, powertoysWindowName);
} }
private void DragWithShift(Pane settingsView, (int Dx, int Dy) offSet)
{
Session.PressKey(Key.Shift);
settingsView.DragAndHold(offSet.Dx, offSet.Dy);
Task.Delay(1000).Wait(); // Wait for drag to start (optional)
settingsView.ReleaseDrag();
Task.Delay(1000).Wait(); // Wait after drag (optional)
Session.ReleaseKey(Key.Shift);
}
private static readonly CustomLayouts.CustomLayoutListWrapper CustomLayoutsList = new CustomLayouts.CustomLayoutListWrapper private static readonly CustomLayouts.CustomLayoutListWrapper CustomLayoutsList = new CustomLayouts.CustomLayoutListWrapper
{ {
CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper> CustomLayouts = new List<CustomLayouts.CustomLayoutWrapper>
@@ -253,11 +272,14 @@ namespace UITests_FancyZones
this.Scroll(9, "Down"); // Pull the setting page up to make sure the setting is visible this.Scroll(9, "Down"); // Pull the setting page up to make sure the setting is visible
bool switchWindowEnable = TestContext.TestName == "TestSwitchShortCutDisable" ? false : true; bool switchWindowEnable = TestContext.TestName == "TestSwitchShortCutDisable" ? false : true;
this.Find<ToggleSwitch>("Switch between windows in the current zone").Toggle(switchWindowEnable); this.Find<ToggleSwitch>("FancyZonesWindowSwitchingToggle").Toggle(switchWindowEnable);
Task.Delay(500).Wait(); // Wait for the setting to be applied // Go back and forth to make sure settings applied
this.Scroll(9, "Up"); // Pull the setting page down to make sure the setting is visible this.Find<NavigationViewItem>("Workspaces").Click();
this.Find<Button>("Launch layout editor").Click(false, 500, 5000); Task.Delay(200).Wait();
this.Find<NavigationViewItem>("FancyZones").Click();
this.Find<Button>("Open layout editor").Click(false, 500, 5000);
this.Session.Attach(PowerToysModule.FancyZone); this.Session.Attach(PowerToysModule.FancyZone);
// pipeline machine may have an unstable delays, causing the custom layout to be unavailable as we set. then A retry is required. // pipeline machine may have an unstable delays, causing the custom layout to be unavailable as we set. then A retry is required.
@@ -273,7 +295,7 @@ namespace UITests_FancyZones
this.Find<Microsoft.PowerToys.UITest.Button>("Close").Click(); this.Find<Microsoft.PowerToys.UITest.Button>("Close").Click();
this.Session.Attach(PowerToysModule.PowerToysSettings); this.Session.Attach(PowerToysModule.PowerToysSettings);
SetupCustomLayouts(); SetupCustomLayouts();
this.Find<Microsoft.PowerToys.UITest.Button>("Launch layout editor").Click(false, 5000, 5000); this.Find<Microsoft.PowerToys.UITest.Button>("Open layout editor").Click(false, 5000, 5000);
this.Session.Attach(PowerToysModule.FancyZone); this.Session.Attach(PowerToysModule.FancyZone);
// customLayoutData = FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.GetData(); // customLayoutData = FancyZonesEditorHelper.Files.CustomLayoutsIOHelper.GetData();
@@ -301,11 +323,11 @@ namespace UITests_FancyZones
Task.Delay(1000).Wait(); Task.Delay(1000).Wait();
this.Find<ToggleSwitch>("Enable Hosts File Editor").Toggle(true); this.Find<ToggleSwitch>("Enable Hosts File Editor").Toggle(true);
this.Find<ToggleSwitch>("Launch as administrator").Toggle(launchAsAdmin); this.Find<ToggleSwitch>("Open as administrator").Toggle(launchAsAdmin);
this.Find<ToggleSwitch>("Show a warning at startup").Toggle(showWarning); this.Find<ToggleSwitch>("Show a warning at startup").Toggle(showWarning);
// launch Hosts File Editor // launch Hosts File Editor
this.Find<Button>("Launch Hosts File Editor").Click(); this.Find<Button>("Open Hosts File Editor").Click();
Task.Delay(5000).Wait(); Task.Delay(5000).Wait();
} }

View File

@@ -124,9 +124,6 @@ public class PeekFilePreviewTests : UITestBase
settings["properties"] = properties; settings["properties"] = properties;
}); });
// Disable all modules except Peek in global settings
SettingsConfigHelper.ConfigureGlobalModuleSettings("Peek");
Debug.WriteLine("Successfully updated all settings - Peek shortcut configured and all modules except Peek disabled"); Debug.WriteLine("Successfully updated all settings - Peek shortcut configured and all modules except Peek disabled");
} }
catch (Exception ex) catch (Exception ex)
@@ -138,6 +135,7 @@ public class PeekFilePreviewTests : UITestBase
[TestInitialize] [TestInitialize]
public void TestInitialize() public void TestInitialize()
{ {
RestartScopeExe("Peek");
Session.CloseMainWindow(); Session.CloseMainWindow();
SendKeys(Key.Win, Key.M); SendKeys(Key.Win, Key.M);
} }

View File

@@ -192,7 +192,10 @@
x:Uid="FancyZones_WindowSwitching_GroupSettings" x:Uid="FancyZones_WindowSwitching_GroupSettings"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}" HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}"
IsExpanded="True"> IsExpanded="True">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.WindowSwitching, Mode=TwoWay}" /> <ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.Name="FancyZonesWindowSwitchingToggle"
IsOn="{x:Bind ViewModel.WindowSwitching, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items> <tkcontrols:SettingsExpander.Items>
<!-- HACK: For some weird reason, a Shortcut Control is not working correctly if it's the first item in the expander, so we add an invisible card as the first one. --> <!-- HACK: For some weird reason, a Shortcut Control is not working correctly if it's the first item in the expander, so we add an invisible card as the first one. -->
<tkcontrols:SettingsCard Name="FancyZonesWindowSwitchingPlaceholder" Visibility="Collapsed" /> <tkcontrols:SettingsCard Name="FancyZonesWindowSwitchingPlaceholder" Visibility="Collapsed" />
@@ -259,7 +262,10 @@
Name="FancyZonesQuickLayoutSwitch" Name="FancyZonesQuickLayoutSwitch"
x:Uid="FancyZones_QuickLayoutSwitch" x:Uid="FancyZones_QuickLayoutSwitch"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}"> HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.QuickLayoutSwitch, Mode=TwoWay}" /> <ToggleSwitch
x:Uid="ToggleSwitch"
AutomationProperties.Name="FancyZonesQuickLayoutSwitch"
IsOn="{x:Bind ViewModel.QuickLayoutSwitch, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items> <tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard <tkcontrols:SettingsCard
Name="FancyZonesFlashZonesOnQuickSwitch" Name="FancyZonesFlashZonesOnQuickSwitch"