Compare commits

...

42 Commits

Author SHA1 Message Date
Leilei Zhang
f8934769c9 remove all 2025-07-21 15:50:37 +08:00
Leilei Zhang
6d80dce74c remove base line image 2025-07-21 15:27:04 +08:00
Leilei Zhang
393fb15019 pdf need more time to load 2025-07-21 14:38:29 +08:00
Leilei Zhang
1d0cee997b use hot key clean screnn 2025-07-21 13:09:54 +08:00
Leilei Zhang
28d585fef6 don't close explorer 2025-07-21 12:44:33 +08:00
Leilei Zhang
65e67f4098 add diagnosticsEnabled 2025-07-21 11:26:02 +08:00
Leilei Zhang
68f474ef64 update project 2025-07-21 10:18:35 +08:00
Leilei Zhang
f1db717b6d update baseline image name 2025-07-21 10:05:52 +08:00
Leilei Zhang
d90c1954b0 update close explorer logic 2025-07-18 18:43:31 +08:00
Leilei Zhang
b0d7bfef7c Merge branch 'main' of https://github.com/microsoft/PowerToys into leilzh/peekuitests 2025-07-18 16:59:45 +08:00
Leilei Zhang
c8cea4a017 exit cmdpal 2025-07-18 16:57:00 +08:00
Leilei Zhang
a16813b73b fix error 2025-07-18 15:50:03 +08:00
Leilei Zhang
1fbb784bea add win10 explorer find 2025-07-18 15:19:41 +08:00
Leilei Zhang
421a7a62d5 update logic 2025-07-18 14:27:00 +08:00
Leilei Zhang
1e0b6d5e38 fix merge error 2025-07-18 11:22:39 +08:00
Leilei Zhang
3bee31bfd7 Merge branch 'main' of https://github.com/microsoft/PowerToys into leilzh/peekuitests 2025-07-18 10:33:01 +08:00
Leilei Zhang
a89d8476f8 use compare 2025-07-18 09:53:42 +08:00
Leilei Zhang
19ada53ce3 get image for testing 2025-07-17 15:58:16 +08:00
Leilei Zhang
0654edb18a update default program tests 2025-07-17 15:49:56 +08:00
Leilei Zhang
7d02867b60 add enter 2025-07-17 13:41:16 +08:00
Leilei Zhang
b05cf8bf9f update common function 2025-07-17 12:23:03 +08:00
Leilei Zhang
522f12a0b2 clean windows 2025-07-17 11:21:30 +08:00
Leilei Zhang
d12bbf0cbf close settings page before testing 2025-07-17 10:18:37 +08:00
Leilei Zhang
f067a925f6 add more tests 2025-07-16 20:41:35 +08:00
Leilei Zhang
37c690b216 update screenshot name 2025-07-16 17:49:08 +08:00
Leilei Zhang
f6ff5064a3 use all 2025-07-16 17:05:20 +08:00
Leilei Zhang
78884bb587 use wrong flag 2025-07-16 15:52:16 +08:00
Leilei Zhang
d2b81450e3 add retry 2025-07-16 14:59:18 +08:00
Leilei Zhang
425883508f update image pipeline 2025-07-16 14:09:25 +08:00
Leilei Zhang
41ff1f8cf7 add more image and add long time 2025-07-16 13:57:53 +08:00
Leilei Zhang
2ee40a99d5 exit command paletter 2025-07-16 12:52:30 +08:00
Leilei Zhang
943d79bb83 clean code 2025-07-16 10:58:43 +08:00
Leilei Zhang
e42a1b8753 add test run title 2025-07-16 10:22:53 +08:00
Leilei Zhang
ed424faea5 add png 2025-07-16 10:11:56 +08:00
Leilei Zhang
85db62e946 add more time 2025-07-16 00:09:18 +08:00
Leilei Zhang
96de5d7a87 add png 2025-07-15 23:00:54 +08:00
Leilei Zhang
8a1aff68c1 add pin and unpin 2025-07-15 21:17:22 +08:00
Leilei Zhang
f19c2e1a78 create image 2025-07-15 09:34:33 +08:00
Leilei Zhang
1dab45c87c check file 2025-07-14 21:44:43 +08:00
Leilei Zhang
0040112855 add delay 2025-07-14 19:48:00 +08:00
Leilei Zhang
6205e25eb5 add delay 2025-07-14 18:54:44 +08:00
Leilei Zhang
dbe3ac6077 add peek ui tests 2025-07-14 17:50:41 +08:00
15 changed files with 1006 additions and 30 deletions

View File

@@ -1615,6 +1615,8 @@ svgz
SVSI
SWFO
SWP
SWPNOSIZE
SWPNOZORDER
SWRESTORE
symbolrequestprod
SYMCACHE

View File

@@ -736,6 +736,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagedCsWin32", "src\commo
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerRenameUITest", "src\modules\powerrename\PowerRenameUITest\PowerRenameUITest.csproj", "{9D3F3793-EFE3-4525-8782-238015DABA62}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Peek.UITests", "src\modules\peek\Peek.UITests\Peek.UITests.csproj", "{BCDC7246-F4F8-4EED-8DE6-037AA2E7C6D1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Core.ViewModels", "src\modules\cmdpal\Microsoft.CmdPal.Core.ViewModels\Microsoft.CmdPal.Core.ViewModels.csproj", "{24133F7F-C1D1-DE04-EFA8-F5D5467FE027}"
@@ -2740,6 +2742,14 @@ Global
{9D3F3793-EFE3-4525-8782-238015DABA62}.Release|ARM64.Build.0 = Release|ARM64
{9D3F3793-EFE3-4525-8782-238015DABA62}.Release|x64.ActiveCfg = Release|x64
{9D3F3793-EFE3-4525-8782-238015DABA62}.Release|x64.Build.0 = Release|x64
{BCDC7246-F4F8-4EED-8DE6-037AA2E7C6D1}.Debug|ARM64.ActiveCfg = Debug|ARM64
{BCDC7246-F4F8-4EED-8DE6-037AA2E7C6D1}.Debug|ARM64.Build.0 = Debug|ARM64
{BCDC7246-F4F8-4EED-8DE6-037AA2E7C6D1}.Debug|x64.ActiveCfg = Debug|x64
{BCDC7246-F4F8-4EED-8DE6-037AA2E7C6D1}.Debug|x64.Build.0 = Debug|x64
{BCDC7246-F4F8-4EED-8DE6-037AA2E7C6D1}.Release|ARM64.ActiveCfg = Release|ARM64
{BCDC7246-F4F8-4EED-8DE6-037AA2E7C6D1}.Release|ARM64.Build.0 = Release|ARM64
{BCDC7246-F4F8-4EED-8DE6-037AA2E7C6D1}.Release|x64.ActiveCfg = Release|x64
{BCDC7246-F4F8-4EED-8DE6-037AA2E7C6D1}.Release|x64.Build.0 = Release|x64
{840455DF-5634-51BB-D937-9D7D32F0B0C2}.Debug|ARM64.ActiveCfg = Debug|ARM64
{840455DF-5634-51BB-D937-9D7D32F0B0C2}.Debug|ARM64.Build.0 = Debug|ARM64
{840455DF-5634-51BB-D937-9D7D32F0B0C2}.Debug|x64.ActiveCfg = Debug|x64
@@ -3035,6 +3045,7 @@ Global
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {3846508C-77EB-4034-A702-F8BB263C4F79}
{24133F7F-C1D1-DE04-EFA8-F5D5467FE027} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{9D3F3793-EFE3-4525-8782-238015DABA62} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}
{BCDC7246-F4F8-4EED-8DE6-037AA2E7C6D1} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20}
{840455DF-5634-51BB-D937-9D7D32F0B0C2} = {7520A2FE-00A2-49B8-83ED-DB216E874C04}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution

View File

@@ -364,7 +364,7 @@ namespace Microsoft.PowerToys.UITest
/// Save UI Element to a PNG file.
/// </summary>
/// <param name="path">the full path</param>
internal void SaveToPngFile(string path)
public void SaveToPngFile(string path)
{
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method SaveToPngFile with parameter: path = {path}");
this.windowsElement.GetScreenshot().SaveAsFile(path);

View File

@@ -91,15 +91,12 @@ namespace Microsoft.PowerToys.UITest
}
/// <summary>
/// Exit a exe.
/// Exit a exe by Name.
/// </summary>
/// <param name="appPath">The path to the application executable.</param>
public void ExitExe(string appPath)
/// <param name="processName">The path to the application executable.</param>
public void ExitExeByName(string processName)
{
// Exit Exe
string exeName = Path.GetFileNameWithoutExtension(appPath);
Process[] processes = Process.GetProcessesByName(exeName);
Process[] processes = Process.GetProcessesByName(processName);
foreach (Process process in processes)
{
try
@@ -114,6 +111,18 @@ namespace Microsoft.PowerToys.UITest
}
}
/// <summary>
/// Exit a exe.
/// </summary>
/// <param name="appPath">The path to the application executable.</param>
public void ExitExe(string appPath)
{
// Exit Exe
string exeName = Path.GetFileNameWithoutExtension(appPath);
ExitExeByName(exeName);
}
/// <summary>
/// Starts a new exe and takes control of it.
/// </summary>
@@ -122,26 +131,34 @@ namespace Microsoft.PowerToys.UITest
public void StartExe(string appPath, string[]? args = null)
{
var opts = new AppiumOptions();
opts.AddAdditionalCapability("app", appPath);
if (args != null && args.Length > 0)
if (scope == PowerToysModule.PowerToysSettings)
{
// Build command line arguments string
string argsString = string.Join(" ", args.Select(arg =>
TryLaunchPowerToysSettings(opts);
}
else
{
opts.AddAdditionalCapability("app", appPath);
if (args != null && args.Length > 0)
{
// Quote arguments that contain spaces
if (arg.Contains(' '))
// Build command line arguments string
string argsString = string.Join(" ", args.Select(arg =>
{
return $"\"{arg}\"";
}
// Quote arguments that contain spaces
if (arg.Contains(' '))
{
return $"\"{arg}\"";
}
return arg;
}));
return arg;
}));
opts.AddAdditionalCapability("appArguments", argsString);
opts.AddAdditionalCapability("appArguments", argsString);
}
}
this.Driver = NewWindowsDriver(opts);
Driver = NewWindowsDriver(opts);
}
private void TryLaunchPowerToysSettings(AppiumOptions opts)
@@ -150,15 +167,18 @@ namespace Microsoft.PowerToys.UITest
var runnerProcessInfo = new ProcessStartInfo
{
FileName = locationPath + this.runnerPath,
FileName = locationPath + runnerPath,
Verb = "runas",
Arguments = "--open-settings",
};
this.ExitExe(runnerProcessInfo.FileName);
this.runner = Process.Start(runnerProcessInfo);
ExitExe(runnerProcessInfo.FileName);
runner = Process.Start(runnerProcessInfo);
Thread.Sleep(5000);
// Exit CmdPal UI before launching new process if use installer for test
ExitExeByName("Microsoft.CmdPal.UI");
if (root != null)
{
const int maxRetries = 5;
@@ -168,7 +188,7 @@ namespace Microsoft.PowerToys.UITest
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
var settingsWindow = ApiHelper.FindDesktopWindowHandler(
new[] { windowName, AdministratorPrefix + windowName });
[windowName, AdministratorPrefix + windowName]);
if (settingsWindow.Count > 0)
{

View File

@@ -22,6 +22,8 @@ namespace Microsoft.PowerToys.UITest
public bool IsInPipeline { get; }
public string? ScreenshotDirectory { get; set; }
public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List<MonitorInfoData.MonitorInfoDataWrapper>() };
private readonly PowerToysModule scope;
@@ -29,7 +31,6 @@ namespace Microsoft.PowerToys.UITest
private readonly string[]? commandLineArgs;
private SessionHelper? sessionHelper;
private System.Threading.Timer? screenshotTimer;
private string? screenshotDirectory;
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified, string[]? commandLineArgs = null)
{
@@ -58,11 +59,11 @@ namespace Microsoft.PowerToys.UITest
CloseOtherApplications();
if (IsInPipeline)
{
screenshotDirectory = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "UITestScreenshots_" + Guid.NewGuid().ToString());
Directory.CreateDirectory(screenshotDirectory);
ScreenshotDirectory = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "UITestScreenshots_" + Guid.NewGuid().ToString());
Directory.CreateDirectory(ScreenshotDirectory);
// 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));
// Escape Popups before starting
System.Windows.Forms.SendKeys.SendWait("{ESC}");
@@ -415,9 +416,9 @@ namespace Microsoft.PowerToys.UITest
protected void AddScreenShotsToTestResultsDirectory()
{
if (screenshotDirectory != null)
if (ScreenshotDirectory != null)
{
foreach (string file in Directory.GetFiles(screenshotDirectory))
foreach (string file in Directory.GetFiles(ScreenshotDirectory))
{
this.TestContext.AddResultFile(file);
}
@@ -627,6 +628,23 @@ namespace Microsoft.PowerToys.UITest
Console.WriteLine($"Failed to change display resolution. Error code: {result}");
}
}
// Windows API for moving windows
[DllImport("user32.dll")]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
private const uint SWPNOSIZE = 0x0001;
private const uint SWPNOZORDER = 0x0004;
public static void MoveWindow(Element window, int x, int y)
{
var windowHandle = IntPtr.Parse(window.GetAttribute("NativeWindowHandle") ?? "0", System.Globalization.CultureInfo.InvariantCulture);
if (windowHandle != IntPtr.Zero)
{
SetWindowPos(windowHandle, IntPtr.Zero, x, y, 0, 0, SWPNOSIZE | SWPNOZORDER);
Task.Delay(500).Wait();
}
}
}
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<RootNamespace>PowerToys.Peek.UITests</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<RunVSTest>false</RunVSTest>
</PropertyGroup>
<PropertyGroup>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\tests\Peek.UITests\</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
<ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="TestAssets\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,864 @@
// 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.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Peek.UITests;
[TestClass]
public class PeekFilePreviewTests : UITestBase
{
// Timeout constants for better maintainability
private const int ExplorerOpenTimeoutSeconds = 15;
private const int PeekWindowTimeoutSeconds = 15;
private const int ExplorerLoadDelayMs = 3000;
private const int ExplorerCheckIntervalMs = 1000;
private const int PeekCheckIntervalMs = 1000;
private const int PeekInitializeDelayMs = 3000;
private const int MaxRetryAttempts = 3;
private const int RetryDelayMs = 3000;
private const int PinActionDelayMs = 500;
public PeekFilePreviewTests()
: base(PowerToysModule.PowerToysSettings, WindowSize.Small_Vertical)
{
}
[TestInitialize]
public void TestInitialize()
{
Session.CloseMainWindow();
SendKeys(Key.Win, Key.M);
}
[TestMethod("Peek.FilePreview.Folder")]
[TestCategory("Preview files")]
public void PeekFolderFilePreview()
{
string folderFullPath = Path.GetFullPath(@".\TestAssets");
var peekWindow = OpenPeekWindow(folderFullPath);
Assert.IsNotNull(peekWindow);
Assert.IsNotNull(peekWindow.Find<TextBlock>("File Type: File folder", 500), "Folder preview should be loaded successfully");
ClosePeekAndExplorer();
}
/// <summary>
/// Test JPEG image preview
/// </summary>
[TestMethod("Peek.FilePreview.JPEGImage")]
[TestCategory("Preview files")]
public void PeekJPEGImagePreview()
{
string imagePath = Path.GetFullPath(@".\TestAssets\2.jpg");
TestSingleFilePreview(imagePath, "2");
}
/// <summary>
/// Test PDF document preview
/// </summary>
[TestMethod("Peek.FilePreview.PDFDocument")]
[TestCategory("Preview files")]
public void PeekPDFDocumentPreview()
{
string pdfPath = Path.GetFullPath(@".\TestAssets\3.pdf");
TestSingleFilePreview(pdfPath, "3", 10000);
}
/// <summary>
/// Test QOI image preview
/// </summary>
[TestMethod("Peek.FilePreview.QOIImage")]
[TestCategory("Preview files")]
public void PeekQOIImagePreview()
{
string qoiPath = Path.GetFullPath(@".\TestAssets\4.qoi");
TestSingleFilePreview(qoiPath, "4");
}
/// <summary>
/// Test C++ source code preview
/// </summary>
[TestMethod("Peek.FilePreview.CPPSourceCode")]
[TestCategory("Preview files")]
public void PeekCPPSourceCodePreview()
{
string cppPath = Path.GetFullPath(@".\TestAssets\5.cpp");
TestSingleFilePreview(cppPath, "5");
}
/// <summary>
/// Test Markdown document preview
/// </summary>
[TestMethod("Peek.FilePreview.MarkdownDocument")]
[TestCategory("Preview files")]
public void PeekMarkdownDocumentPreview()
{
string markdownPath = Path.GetFullPath(@".\TestAssets\6.md");
TestSingleFilePreview(markdownPath, "6");
}
/// <summary>
/// Test ZIP archive preview
/// </summary>
[TestMethod("Peek.FilePreview.ZIPArchive")]
[TestCategory("Preview files")]
public void PeekZIPArchivePreview()
{
string zipPath = Path.GetFullPath(@".\TestAssets\7.zip");
TestSingleFilePreview(zipPath, "7");
}
/// <summary>
/// Test PNG image preview
/// </summary>
[TestMethod("Peek.FilePreview.PNGImage")]
[TestCategory("Preview files")]
public void PeekPNGImagePreview()
{
string pngPath = Path.GetFullPath(@".\TestAssets\8.png");
TestSingleFilePreview(pngPath, "8");
}
/// <summary>
/// Test window pinning functionality - pin window and switch between different sized images
/// Verify the window stays at the same place and the same size
/// </summary>
[TestMethod("Peek.WindowPinning.PinAndSwitchImages")]
[TestCategory("Window Pinning")]
public void TestPinWindowAndSwitchImages()
{
// Use two different image files with different size
string firstImagePath = Path.GetFullPath(@".\TestAssets\8.png");
string secondImagePath = Path.GetFullPath(@".\TestAssets\2.jpg"); // Different format/size
// Open first image
var initialWindow = OpenPeekWindow(firstImagePath);
var originalBounds = GetWindowBounds(initialWindow);
// Move window to a custom position to test pin functionality
NativeMethods.MoveWindow(initialWindow, originalBounds.X + 100, originalBounds.Y + 50);
var movedBounds = GetWindowBounds(initialWindow);
// Pin the window
PinWindow();
// Close current peek
ClosePeekAndExplorer();
// Open second image with different size
var secondWindow = OpenPeekWindow(secondImagePath);
var finalBounds = GetWindowBounds(secondWindow);
// Verify window position and size remained the same as the moved position
Assert.AreEqual(movedBounds.X, finalBounds.X, 5, "Window X position should remain the same when pinned");
Assert.AreEqual(movedBounds.Y, finalBounds.Y, 5, "Window Y position should remain the same when pinned");
Assert.AreEqual(movedBounds.Width, finalBounds.Width, 10, "Window width should remain the same when pinned");
Assert.AreEqual(movedBounds.Height, finalBounds.Height, 10, "Window height should remain the same when pinned");
ClosePeekAndExplorer();
}
/// <summary>
/// Test window pinning persistence - pin window, close and reopen Peek
/// Verify the new window is opened at the same place and the same size as before
/// </summary>
[TestMethod("Peek.WindowPinning.PinAndReopen")]
[TestCategory("Window Pinning")]
public void TestPinWindowAndReopen()
{
string imagePath = Path.GetFullPath(@".\TestAssets\8.png");
// Open image and pin window
var initialWindow = OpenPeekWindow(imagePath);
var originalBounds = GetWindowBounds(initialWindow);
// Move window to a custom position to test pin persistence
NativeMethods.MoveWindow(initialWindow, originalBounds.X + 150, originalBounds.Y + 75);
var movedBounds = GetWindowBounds(initialWindow);
// Pin the window
PinWindow();
// Close peek
ClosePeekAndExplorer();
Thread.Sleep(1000); // Wait for window to close completely
// Reopen the same image
var reopenedWindow = OpenPeekWindow(imagePath);
var finalBounds = GetWindowBounds(reopenedWindow);
// Verify window position and size are restored to the moved position
Assert.AreEqual(movedBounds.X, finalBounds.X, 5, "Window X position should be restored when pinned");
Assert.AreEqual(movedBounds.Y, finalBounds.Y, 5, "Window Y position should be restored when pinned");
Assert.AreEqual(movedBounds.Width, finalBounds.Width, 10, "Window width should be restored when pinned");
Assert.AreEqual(movedBounds.Height, finalBounds.Height, 10, "Window height should be restored when pinned");
ClosePeekAndExplorer();
}
/// <summary>
/// Test window unpinning - unpin window and switch to different file
/// Verify the window is moved to the default place
/// </summary>
[TestMethod("Peek.WindowPinning.UnpinAndSwitchFiles")]
[TestCategory("Window Pinning")]
public void TestUnpinWindowAndSwitchFiles()
{
string firstFilePath = Path.GetFullPath(@".\TestAssets\8.png");
string secondFilePath = Path.GetFullPath(@".\TestAssets\2.jpg");
// Open first file and pin window
var pinnedWindow = OpenPeekWindow(firstFilePath);
var originalBounds = GetWindowBounds(pinnedWindow);
// Move window to a custom position
NativeMethods.MoveWindow(pinnedWindow, originalBounds.X + 200, originalBounds.Y + 100);
var movedBounds = GetWindowBounds(pinnedWindow);
// Calculate the center point of the moved window
var movedCenter = Session.GetMainWindowCenter();
// Pin the window first
PinWindow();
// Unpin the window
UnpinWindow();
// Close current peek
ClosePeekAndExplorer();
// Open different file (different size)
var unpinnedWindow = OpenPeekWindow(secondFilePath);
var unpinnedBounds = GetWindowBounds(unpinnedWindow);
// Calculate the center point of the unpinned window
var unpinnedCenter = Session.GetMainWindowCenter();
// Verify window size is different (since it's a different file type)
bool sizeChanged = Math.Abs(movedBounds.Width - unpinnedBounds.Width) > 10 ||
Math.Abs(movedBounds.Height - unpinnedBounds.Height) > 10;
// Verify window center moved to default position (should be different from moved center)
bool centerChanged = Math.Abs(movedCenter.CenterX - unpinnedCenter.CenterX) > 50 ||
Math.Abs(movedCenter.CenterY - unpinnedCenter.CenterY) > 50;
Assert.IsTrue(sizeChanged, "Window size should be different for different file types");
Assert.IsTrue(centerChanged, "Window center should move to default position when unpinned");
ClosePeekAndExplorer();
}
/// <summary>
/// Test unpinned window behavior - unpin window, close and reopen Peek
/// Verify the new window is opened on the default place
/// </summary>
[TestMethod("Peek.WindowPinning.UnpinAndReopen")]
[TestCategory("Window Pinning")]
public void TestUnpinWindowAndReopen()
{
string imagePath = Path.GetFullPath(@".\TestAssets\8.png");
// Open image, pin it first, then unpin
var initialWindow = OpenPeekWindow(imagePath);
var originalBounds = GetWindowBounds(initialWindow);
// Move window to a custom position
NativeMethods.MoveWindow(initialWindow, originalBounds.X + 250, originalBounds.Y + 125);
var movedBounds = GetWindowBounds(initialWindow);
// Pin then unpin to ensure we test the unpinned state
PinWindow();
UnpinWindow();
// Close peek
ClosePeekAndExplorer();
// Reopen the same image
var reopenedWindow = OpenPeekWindow(imagePath);
var reopenedBounds = GetWindowBounds(reopenedWindow);
// Verify window opened at default position (not the previous moved position)
bool openedAtDefault = Math.Abs(movedBounds.X - reopenedBounds.X) > 50 ||
Math.Abs(movedBounds.Y - reopenedBounds.Y) > 50;
Assert.IsTrue(openedAtDefault, "Unpinned window should open at default position, not previous moved position");
ClosePeekAndExplorer();
}
/// <summary>
/// Test opening file with default program by clicking a button
/// </summary>
[TestMethod("Peek.OpenWithDefaultProgram.ClickButton")]
[TestCategory("Open with default program")]
public void TestOpenWithDefaultProgramByButton()
{
string zipPath = Path.GetFullPath(@".\TestAssets\7.zip");
// Open zip file with Peek
var peekWindow = OpenPeekWindow(zipPath);
// Find and click the "Open with default program" button
var openButton = FindLaunchButton();
Assert.IsNotNull(openButton, "Open with default program button should be found");
// Click the button to open with default program
openButton.Click();
// Wait a moment for the default program to launch
Thread.Sleep(2000);
// Verify that the default program process has started (check for Explorer opening 7-zip)
bool defaultProgramLaunched = CheckIfExplorerLaunched();
Assert.IsTrue(defaultProgramLaunched, "Default program (Explorer/7-zip) should be launched after clicking the button");
ClosePeekAndExplorer();
}
/// <summary>
/// Test opening file with default program by pressing Enter key
/// </summary>
[TestMethod("Peek.OpenWithDefaultProgram.PressEnter")]
[TestCategory("Open with default program")]
public void TestOpenWithDefaultProgramByEnter()
{
string zipPath = Path.GetFullPath(@".\TestAssets\7.zip");
// Open zip file with Peek
var peekWindow = OpenPeekWindow(zipPath);
// Press Enter key to open with default program
SendKeys(Key.Enter);
// Wait a moment for the default program to launch
Thread.Sleep(2000);
// Verify that the default program process has started (check for Explorer opening 7-zip)
bool defaultProgramLaunched = CheckIfExplorerLaunched();
Assert.IsTrue(defaultProgramLaunched, "Default program (Explorer/7-zip) should be launched after pressing Enter");
ClosePeekAndExplorer();
}
/// <summary>
/// Test switching between files in a folder using Left and Right arrow keys
/// </summary>
[TestMethod("Peek.FileNavigation.SwitchFilesWithArrowKeys")]
[TestCategory("File Navigation")]
public void TestSwitchFilesWithArrowKeys()
{
// Get all files in TestAssets folder, ordered alphabetically
var testFiles = GetTestAssetFiles();
// Start with the first file in the TestAssets folder
string firstFilePath = testFiles[0];
var peekWindow = OpenPeekWindow(firstFilePath);
// Keep track of visited files to ensure we can navigate through all
var visitedFiles = new List<string> { Path.GetFileNameWithoutExtension(firstFilePath) };
// Navigate forward through files using Right arrow
for (int i = 1; i < testFiles.Count; i++)
{
// Press Right arrow to go to next file
SendKeys(Key.Right);
// Wait for file to load
Thread.Sleep(2000);
// Try to determine current file from window title
var currentWindow = peekWindow.Name;
string expectedFileName = Path.GetFileNameWithoutExtension(testFiles[i]);
if (!string.IsNullOrEmpty(currentWindow) && currentWindow.StartsWith(expectedFileName, StringComparison.Ordinal))
{
visitedFiles.Add(expectedFileName);
}
}
// Verify we navigated through the expected number of files
Assert.AreEqual(testFiles.Count, visitedFiles.Count, $"Should have navigated through all {testFiles.Count} files, but only visited {visitedFiles.Count} files: {string.Join(", ", visitedFiles)}");
// Navigate backward using Left arrow to verify reverse navigation
for (int i = testFiles.Count - 2; i >= 0; i--)
{
SendKeys(Key.Left);
// Wait for file to load
Thread.Sleep(2000);
// Try to determine current file from window title during backward navigation
var currentWindow = peekWindow.Name;
string expectedFileName = Path.GetFileNameWithoutExtension(testFiles[i]);
if (!string.IsNullOrEmpty(currentWindow) && currentWindow.StartsWith(expectedFileName, StringComparison.Ordinal))
{
// Remove the last visited file (going backward)
if (visitedFiles.Count > 1)
{
visitedFiles.RemoveAt(visitedFiles.Count - 1);
}
}
}
// Verify backward navigation worked - should be back to the first file
Assert.AreEqual(1, visitedFiles.Count, $"After backward navigation, should be back to first file only. Remaining files: {string.Join(", ", visitedFiles)}");
ClosePeekAndExplorer();
}
/// <summary>
/// Test switching between multiple selected files
/// Select first 3 files in Explorer, open with Peek, verify you can switch only between selected files using arrow keys
/// </summary>
[TestMethod("Peek.FileNavigation.SwitchBetweenSelectedFiles")]
[TestCategory("File Navigation")]
public void TestSwitchBetweenSelectedFiles()
{
// Get first 3 files in TestAssets folder, ordered alphabetically
var allFiles = GetTestAssetFiles();
var selectedFiles = allFiles.Take(3).ToList();
// Open Explorer and select the first file
Session.StartExe("explorer.exe", $"/select,\"{selectedFiles[0]}\"");
// Wait for Explorer to open and select the first file
WaitForExplorerWindow(selectedFiles[0]);
// Give Explorer time to fully load
Thread.Sleep(2000);
// Use Shift+Down to extend selection to include the next 2 files
SendKeys(Key.Shift, Key.Down); // Extend to second file
Thread.Sleep(300);
SendKeys(Key.Shift, Key.Down); // Extend to third file
Thread.Sleep(300);
// Now we should have the first 3 files selected, open Peek
SendPeekHotkeyWithRetry();
// Find the peek window (should open with last selected file when multiple files are selected)
var peekWindow = FindPeekWindow(selectedFiles[2]); // Third file (last selected)
string lastFileName = Path.GetFileNameWithoutExtension(selectedFiles[2]);
// Keep track of visited files during navigation (starting from the last file)
var visitedFiles = new List<string> { lastFileName };
var expectedFileNames = selectedFiles.Select(f => Path.GetFileNameWithoutExtension(f)).ToList();
// Test navigation by pressing Left arrow multiple times to verify we only cycle through 3 selected files
var windowTitles = new List<string> { peekWindow.Name };
// Press Left arrow 5 times (more than the 3 selected files) to see if we cycle through only the selected files
for (int i = 0; i < 5; i++)
{
SendKeys(Key.Left);
Thread.Sleep(2000); // Wait for file to load
var currentWindowTitle = peekWindow.Name;
windowTitles.Add(currentWindowTitle);
}
// Analyze the navigation pattern - we should see repetition indicating we're only cycling through 3 files
var uniqueWindowsVisited = windowTitles.Distinct().Count();
// We should see at most 3 unique windows (the 3 selected files), even after 6 navigation steps
Assert.IsTrue(uniqueWindowsVisited <= 3, $"Should only navigate through the 3 selected files, but found {uniqueWindowsVisited} unique windows. " + $"Window titles: {string.Join(" -> ", windowTitles)}");
ClosePeekAndExplorer();
}
private bool CheckIfExplorerLaunched()
{
var possibleTitles = new[]
{
"7.zip - File Explorer",
"7 - File Explorer",
"7",
"7.zip",
};
foreach (var title in possibleTitles)
{
try
{
var explorerWindow = Find(title, 5000, true);
if (explorerWindow != null)
{
return true;
}
}
catch
{
// Continue to next title
}
}
return false;
}
private void OpenAndPeekFile(string fullPath)
{
Session.StartExe("explorer.exe", $"/select,\"{fullPath}\"");
// Wait for Explorer to open and become ready
WaitForExplorerWindow(fullPath);
// Send Peek hotkey with retry mechanism
SendPeekHotkeyWithRetry();
}
private void WaitForExplorerWindow(string filePath)
{
WaitForCondition(
condition: () =>
{
try
{
// Check if Explorer window is open and responsive
var explorerProcesses = Process.GetProcessesByName("explorer")
.Where(p => p.MainWindowHandle != IntPtr.Zero)
.ToList();
if (explorerProcesses.Count != 0)
{
// Give Explorer a moment to fully load the file selection
Thread.Sleep(ExplorerLoadDelayMs);
// Verify the file is accessible
return File.Exists(filePath) || Directory.Exists(filePath);
}
return false;
}
catch (Exception ex)
{
Debug.WriteLine($"WaitForExplorerWindow exception: {ex.Message}");
return false;
}
},
timeoutSeconds: ExplorerOpenTimeoutSeconds,
checkIntervalMs: ExplorerCheckIntervalMs,
timeoutMessage: $"Explorer window did not open for file: {filePath}");
}
private void SendPeekHotkeyWithRetry()
{
for (int attempt = 1; attempt <= MaxRetryAttempts; attempt++)
{
try
{
// Send the Peek hotkey
SendKeys(Key.LCtrl, Key.Space);
// Wait for Peek window to appear
if (WaitForPeekWindow())
{
return; // Success
}
}
catch (Exception ex)
{
Debug.WriteLine($"SendPeekHotkeyWithRetry attempt {attempt} failed: {ex.Message}");
if (attempt == MaxRetryAttempts)
{
throw new InvalidOperationException($"Failed to open Peek after {MaxRetryAttempts} attempts. Last error: {ex.Message}", ex);
}
}
// Wait before retry using Thread.Sleep
Thread.Sleep(RetryDelayMs);
}
throw new InvalidOperationException($"Failed to open Peek after {MaxRetryAttempts} attempts");
}
private bool WaitForPeekWindow()
{
try
{
WaitForCondition(
condition: () =>
{
if (TryFindPeekWindow())
{
// Give Peek a moment to fully initialize using Thread.Sleep
Thread.Sleep(PeekInitializeDelayMs);
return true;
}
return false;
},
timeoutSeconds: PeekWindowTimeoutSeconds,
checkIntervalMs: PeekCheckIntervalMs,
timeoutMessage: "Peek window did not appear");
return true;
}
catch (Exception ex)
{
Debug.WriteLine($"WaitForPeekWindow failed: {ex.Message}");
return false;
}
}
private bool WaitForCondition(Func<bool> condition, int timeoutSeconds, int checkIntervalMs, string timeoutMessage)
{
var timeout = TimeSpan.FromSeconds(timeoutSeconds);
var startTime = DateTime.Now;
while (DateTime.Now - startTime < timeout)
{
try
{
if (condition())
{
return true;
}
}
catch (Exception ex)
{
// Log exception but continue waiting
Debug.WriteLine($"WaitForCondition exception: {ex.Message}");
}
// Use async delay to prevent blocking the thread
Thread.Sleep(checkIntervalMs);
}
throw new TimeoutException($"{timeoutMessage} (timeout: {timeoutSeconds}s)");
}
private bool TryFindPeekWindow()
{
try
{
// Check for Peek process with timeout
var peekProcesses = Process.GetProcessesByName("PowerToys.Peek.UI")
.Where(p => p.MainWindowHandle != IntPtr.Zero);
var foundProcess = peekProcesses.Any();
if (foundProcess)
{
// Additional validation - check if window is responsive
Thread.Sleep(100); // Small delay to ensure window is ready
return true;
}
return false;
}
catch (Exception ex)
{
Debug.WriteLine($"TryFindPeekWindow exception: {ex.Message}");
return false;
}
}
private Element OpenPeekWindow(string filePath)
{
try
{
SendKeys(Key.Enter);
// Open file with Peek
OpenAndPeekFile(filePath);
// Find the Peek window using the common method with timeout
var peekWindow = FindPeekWindow(filePath);
// Attach to the found window with error handling
try
{
Session.Attach(peekWindow.Name);
}
catch (Exception ex)
{
Debug.WriteLine($"Failed to attach to window: {ex.Message}");
}
return peekWindow;
}
catch (Exception ex)
{
Debug.WriteLine($"OpenPeekWindow failed for {filePath}: {ex.Message}");
throw;
}
}
/// <summary>
/// Test a single file preview with visual comparison
/// </summary>
/// <param name="filePath">Full path to the file to test</param>
/// <param name="expectedFileName">Expected file name for visual comparison</param>
private void TestSingleFilePreview(string filePath, string expectedFileName, int? delayMs = null)
{
Element? previewWindow = null;
try
{
Debug.WriteLine($"Testing file preview: {Path.GetFileName(filePath)}");
previewWindow = OpenPeekWindow(filePath);
if (delayMs.HasValue)
{
Thread.Sleep(delayMs.Value); // Allow time for the preview to load
}
Assert.IsNotNull(previewWindow, $"Should open Peek window for {Path.GetFileName(filePath)}");
// Perform visual comparison
VisualAssert.AreEqual(TestContext, previewWindow, expectedFileName);
Debug.WriteLine($"Successfully tested: {Path.GetFileName(filePath)}");
}
finally
{
// Always cleanup in finally block
ClosePeekAndExplorer();
}
}
private Rectangle GetWindowBounds(Element window)
{
if (window.Rect == null)
{
return Rectangle.Empty;
}
else
{
return window.Rect.Value;
}
}
private void PinWindow()
{
// Find pin button using AutomationId
var pinButton = Find(By.AccessibilityId("PinButton"), 2000);
Assert.IsNotNull(pinButton, "Pin button should be found");
pinButton.Click();
Thread.Sleep(PinActionDelayMs); // Wait for pin action to complete
}
private void UnpinWindow()
{
// Find pin button using AutomationId (same button, just toggle the state)
var pinButton = Find(By.AccessibilityId("PinButton"), 2000);
Assert.IsNotNull(pinButton, "Pin button should be found");
pinButton.Click();
Thread.Sleep(PinActionDelayMs); // Wait for unpin action to complete
}
private void ClosePeekAndExplorer()
{
try
{
// Close Peek window
Session.CloseMainWindow();
Thread.Sleep(500);
SendKeys(Key.Win, Key.M);
}
catch (Exception ex)
{
Debug.WriteLine($"Error closing Peek window: {ex.Message}");
}
}
/// <summary>
/// Get all files in TestAssets folder, ordered alphabetically, excluding hidden files
/// </summary>
/// <returns>List of file paths in alphabetical order</returns>
private List<string> GetTestAssetFiles()
{
string testAssetsPath = Path.GetFullPath(@".\TestAssets");
return Directory.GetFiles(testAssetsPath, "*.*", SearchOption.TopDirectoryOnly)
.Where(file => !Path.GetFileName(file).StartsWith('.'))
.OrderBy(file => file)
.ToList();
}
/// <summary>
/// Find Peek window by trying both filename with and without extension
/// </summary>
/// <param name="filePath">Full path to the file</param>
/// <param name="timeout">Timeout in milliseconds</param>
/// <returns>The found Peek window element</returns>
private Element FindPeekWindow(string filePath, int timeout = 5000)
{
string fileName = Path.GetFileName(filePath);
string fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
// Try both window title formats since Windows may show or hide file extensions
string peekWindowTitleWithExt = $"{fileName} - Peek";
string peekWindowTitleWithoutExt = $"{fileNameWithoutExt} - Peek";
Element? peekWindow = null;
try
{
// First try to find the window with extension
peekWindow = Find(peekWindowTitleWithoutExt, timeout, true);
}
catch
{
try
{
// Then try without extension
peekWindow = Find(peekWindowTitleWithExt, timeout, true);
}
catch
{
// If neither works, let it fail with a clear message
Assert.Fail($"Could not find Peek window with title '{peekWindowTitleWithExt}' or '{peekWindowTitleWithoutExt}'");
}
}
Assert.IsNotNull(peekWindow, $"Should find Peek window for file: {Path.GetFileName(filePath)}");
return peekWindow;
}
/// <summary>
/// Helper method to find the launch button with different AccessibilityIds depending on window size
/// </summary>
/// <returns>The launch button element</returns>
private Element? FindLaunchButton()
{
try
{
// Try to find button with ID for larger window first
var button = Find(By.AccessibilityId("LaunchAppButton_Text"), 1000);
if (button != null)
{
return button;
}
}
catch
{
// Try to find button with ID for smaller window
var button = Find(By.AccessibilityId("LaunchAppButton"), 1000);
if (button != null)
{
return button;
}
}
return null;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#include <iostream>
int main() {
std::cout << "Hello, world!" << std::endl;
return 0;
}

View File

@@ -0,0 +1,11 @@
## 简单的 C++ 示例
这是一个最基础的 C++ 程序,它会输出 "Hello, world!"
```cpp
#include <iostream>
int main() {
std::cout << "Hello, world!" << std::endl;
return 0;
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -0,0 +1,21 @@
## Peek
* Open different files to check that they're shown properly
- [x] Image
- [x] Text or dev file
- [x] Markdown file
- [x] PDF
- [x] Archive files (.zip, .tar, .rar)
- [x] Any other not mentioned file (.exe for example) to verify the unsupported file view is shown
* Pinning/unpinning
- [x] Pin the window, switch between images of different size, verify the window stays at the same place and the same size.
- [x] Pin the window, close and reopen Peek, verify the new window is opened at the same place and the same size as before.
- [x] Unpin the window, switch to a different file, verify the window is moved to the default place.
- [x] Unpin the window, close and reopen Peek, verify the new window is opened on the default place.
* Open with a default program
- [x] By clicking a button.
- [x] By pressing enter.
- [x] Switch between files in the folder using `LeftArrow` and `RightArrow`, verify you can switch between all files in the folder.
- [x] Open multiple files, verify you can switch only between selected files.