diff --git a/.pipelines/v2/templates/job-test-project.yml b/.pipelines/v2/templates/job-test-project.yml
index 234f477fb6..9be8f6ca14 100644
--- a/.pipelines/v2/templates/job-test-project.yml
+++ b/.pipelines/v2/templates/job-test-project.yml
@@ -102,7 +102,6 @@ jobs:
uiTests: true
rerunFailedTests: true
testAssemblyVer2: |
- **\UITests-FancyZones.dll
- **\UITests-FancyZonesEditor.dll
+ **\*UITest*.dll
!**\obj\**
!**\ref\**
\ No newline at end of file
diff --git a/.pipelines/verifyDepsJsonLibraryVersions.ps1 b/.pipelines/verifyDepsJsonLibraryVersions.ps1
index e85ca1d991..9ff05c61b1 100644
--- a/.pipelines/verifyDepsJsonLibraryVersions.ps1
+++ b/.pipelines/verifyDepsJsonLibraryVersions.ps1
@@ -91,5 +91,4 @@ if ($totalFailures -gt 0) {
}
Write-Host -ForegroundColor Green "All " $referencedFileVersionsPerDll.keys.Count " libraries are mentioned with the same version across the dependencies.`r`n"
-exit 0
-
+exit 0
\ No newline at end of file
diff --git a/src/common/UITestAutomation/Element/Element.cs b/src/common/UITestAutomation/Element/Element.cs
index 7ca0cf53a5..8a2ec6dc71 100644
--- a/src/common/UITestAutomation/Element/Element.cs
+++ b/src/common/UITestAutomation/Element/Element.cs
@@ -191,7 +191,7 @@ namespace Microsoft.PowerToys.UITest
/// Send Key of the element.
///
/// The Key to Send.
- public void SendKeys(string key)
+ public void InputText(string key)
{
PerformAction((actions, windowElement) =>
{
@@ -218,7 +218,7 @@ namespace Microsoft.PowerToys.UITest
/// The selector to use for finding the element.
/// The timeout in milliseconds.
/// The found element.
- public T Find(By by, int timeoutMS = 3000)
+ public T Find(By by, int timeoutMS = 5000)
where T : Element, new()
{
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
@@ -226,7 +226,7 @@ namespace Microsoft.PowerToys.UITest
// leverage findAll to filter out mismatched elements
var collection = this.FindAll(by, timeoutMS);
- Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}");
+ Assert.IsTrue(collection.Count > 0, $"UI-Element({typeof(T).Name}) not found using selector: {by}");
return collection[0];
}
@@ -239,7 +239,7 @@ namespace Microsoft.PowerToys.UITest
/// The name for finding the element.
/// The timeout in milliseconds.
/// The found element.
- public T Find(string name, int timeoutMS = 3000)
+ public T Find(string name, int timeoutMS = 5000)
where T : Element, new()
{
return this.Find(By.Name(name), timeoutMS);
@@ -252,7 +252,7 @@ namespace Microsoft.PowerToys.UITest
/// The selector to use for finding the element.
/// The timeout in milliseconds.
/// The found element.
- public Element Find(By by, int timeoutMS = 3000)
+ public Element Find(By by, int timeoutMS = 5000)
{
return this.Find(by, timeoutMS);
}
@@ -264,7 +264,7 @@ namespace Microsoft.PowerToys.UITest
/// The name for finding the element.
/// The timeout in milliseconds.
/// The found element.
- public Element Find(string name, int timeoutMS = 3000)
+ public Element Find(string name, int timeoutMS = 5000)
{
return this.Find(By.Name(name), timeoutMS);
}
@@ -276,7 +276,7 @@ namespace Microsoft.PowerToys.UITest
/// The selector to use for finding the elements.
/// The timeout in milliseconds.
/// A read-only collection of the found elements.
- public ReadOnlyCollection FindAll(By by, int timeoutMS = 3000)
+ public ReadOnlyCollection FindAll(By by, int timeoutMS = 5000)
where T : Element, new()
{
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
@@ -308,7 +308,7 @@ namespace Microsoft.PowerToys.UITest
/// The name for finding the element.
/// The timeout in milliseconds.
/// A read-only collection of the found elements.
- public ReadOnlyCollection FindAll(string name, int timeoutMS = 3000)
+ public ReadOnlyCollection FindAll(string name, int timeoutMS = 5000)
where T : Element, new()
{
return this.FindAll(By.Name(name), timeoutMS);
@@ -321,7 +321,7 @@ namespace Microsoft.PowerToys.UITest
/// The selector to use for finding the elements.
/// The timeout in milliseconds.
/// A read-only collection of the found elements.
- public ReadOnlyCollection FindAll(By by, int timeoutMS = 3000)
+ public ReadOnlyCollection FindAll(By by, int timeoutMS = 5000)
{
return this.FindAll(by, timeoutMS);
}
@@ -333,7 +333,7 @@ namespace Microsoft.PowerToys.UITest
/// The name for finding the element.
/// The timeout in milliseconds.
/// A read-only collection of the found elements.
- public ReadOnlyCollection FindAll(string name, int timeoutMS = 3000)
+ public ReadOnlyCollection FindAll(string name, int timeoutMS = 5000)
{
return this.FindAll(By.Name(name), timeoutMS);
}
@@ -360,5 +360,20 @@ namespace Microsoft.PowerToys.UITest
Task.Delay(msPostAction).Wait();
}
}
+
+ ///
+ /// Save UI Element to a PNG file.
+ ///
+ /// the full path
+ internal void SaveToPngFile(string path, bool eraseUserPreferenceColor)
+ {
+ Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method SaveToFile with parameter: path = {path}");
+ this.windowsElement.GetScreenshot().SaveAsFile(path);
+
+ if (eraseUserPreferenceColor)
+ {
+ VisualHelper.EraseUserPreferenceColor(path);
+ }
+ }
}
}
diff --git a/src/common/UITestAutomation/Element/TextBox.cs b/src/common/UITestAutomation/Element/TextBox.cs
index 71f833625d..932f6058b5 100644
--- a/src/common/UITestAutomation/Element/TextBox.cs
+++ b/src/common/UITestAutomation/Element/TextBox.cs
@@ -2,8 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using OpenQA.Selenium;
-
namespace Microsoft.PowerToys.UITest
{
///
@@ -35,8 +33,8 @@ namespace Microsoft.PowerToys.UITest
PerformAction((actions, windowElement) =>
{
// select all text and delete it
- windowElement.SendKeys(Keys.Control + "a");
- windowElement.SendKeys(Keys.Delete);
+ windowElement.SendKeys(OpenQA.Selenium.Keys.Control + "a");
+ windowElement.SendKeys(OpenQA.Selenium.Keys.Delete);
});
}
diff --git a/src/common/UITestAutomation/FindHelper.cs b/src/common/UITestAutomation/FindHelper.cs
index 465f1206a1..00f72edc77 100644
--- a/src/common/UITestAutomation/FindHelper.cs
+++ b/src/common/UITestAutomation/FindHelper.cs
@@ -33,7 +33,7 @@ namespace Microsoft.PowerToys.UITest
public static ReadOnlyCollection? FindAll(Func> findElementsFunc, WindowsDriver? driver, int timeoutMS)
where T : Element, new()
{
- var items = findElementsFunc();
+ var items = FindElementsWithRetry(findElementsFunc, timeoutMS);
var res = items.Select(item =>
{
var element = item as WindowsElement;
@@ -43,6 +43,27 @@ namespace Microsoft.PowerToys.UITest
return new ReadOnlyCollection(res);
}
+ private static ReadOnlyCollection FindElementsWithRetry(Func> findElementsFunc, int timeoutMS)
+ {
+ int retryIntervalMS = 500;
+ timeoutMS = 1;
+ int elapsedTime = 0;
+
+ while (elapsedTime < timeoutMS)
+ {
+ var items = findElementsFunc();
+ if (items.Count > 0)
+ {
+ return items;
+ }
+
+ Task.Delay(retryIntervalMS).Wait();
+ elapsedTime += retryIntervalMS;
+ }
+
+ return new ReadOnlyCollection(new List());
+ }
+
public static T NewElement(WindowsElement? element, WindowsDriver? driver, int timeoutMS)
where T : Element, new()
{
@@ -50,11 +71,6 @@ namespace Microsoft.PowerToys.UITest
Assert.IsNotNull(element, $"New Element {typeof(T).Name} error: element is null.");
T newElement = new T();
- if (timeoutMS > 0)
- {
- // Only set timeout if it is positive value
- driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(timeoutMS);
- }
newElement.SetSession(driver);
newElement.SetWindowsElement(element);
diff --git a/src/common/UITestAutomation/KeyboardHelper.cs b/src/common/UITestAutomation/KeyboardHelper.cs
new file mode 100644
index 0000000000..8a086f1bf7
--- /dev/null
+++ b/src/common/UITestAutomation/KeyboardHelper.cs
@@ -0,0 +1,287 @@
+// 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.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.UITest
+{
+ ///
+ /// Represents keyboard keys.
+ ///
+ public enum Key
+ {
+ Ctrl,
+ Alt,
+ Shift,
+ Tab,
+ Esc,
+ Enter,
+ Win,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+ U,
+ V,
+ W,
+ X,
+ Y,
+ Z,
+ Num0,
+ Num1,
+ Num2,
+ Num3,
+ Num4,
+ Num5,
+ Num6,
+ Num7,
+ Num8,
+ Num9,
+ F1,
+ F2,
+ F3,
+ F4,
+ F5,
+ F6,
+ F7,
+ F8,
+ F9,
+ F10,
+ F11,
+ F12,
+ Space,
+ Backspace,
+ Delete,
+ Insert,
+ Home,
+ End,
+ PageUp,
+ PageDown,
+ Up,
+ Down,
+ Left,
+ Right,
+ Other,
+ }
+
+ ///
+ /// Provides methods for simulating keyboard input.
+ ///
+ internal static class KeyboardHelper
+ {
+ [DllImport("user32.dll")]
+#pragma warning disable SA1300 // Element should begin with upper-case letter
+ private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
+#pragma warning restore SA1300 // Element should begin with upper-case letter
+
+#pragma warning disable SA1310 // Field names should not contain underscore
+ private const byte VK_LWIN = 0x5B;
+ private const uint KEYEVENTF_KEYDOWN = 0x0000;
+ private const uint KEYEVENTF_KEYUP = 0x0002;
+#pragma warning restore SA1310 // Field names should not contain underscore
+
+ ///
+ /// Sends a combination of keys.
+ ///
+ /// The keys to send.
+ public static void SendKeys(params Key[] keys)
+ {
+ string keysToSend = string.Join(string.Empty, keys.Select(TranslateKey));
+ SendWinKeyCombination(keysToSend);
+ }
+
+ ///
+ /// Translates a key to its corresponding SendKeys representation.
+ ///
+ /// The key to translate.
+ /// The SendKeys representation of the key.
+ private static string TranslateKey(Key key)
+ {
+ switch (key)
+ {
+ case Key.Ctrl:
+ return "^";
+ case Key.Alt:
+ return "%";
+ case Key.Shift:
+ return "+";
+ case Key.Tab:
+ return "{TAB}";
+ case Key.Esc:
+ return "{ESC}";
+ case Key.Enter:
+ return "{ENTER}";
+ case Key.Win:
+ return "{WIN}";
+ case Key.Space:
+ return " ";
+ case Key.Backspace:
+ return "{BACKSPACE}";
+ case Key.Delete:
+ return "{DELETE}";
+ case Key.Insert:
+ return "{INSERT}";
+ case Key.Home:
+ return "{HOME}";
+ case Key.End:
+ return "{END}";
+ case Key.PageUp:
+ return "{PGUP}";
+ case Key.PageDown:
+ return "{PGDN}";
+ case Key.Up:
+ return "{UP}";
+ case Key.Down:
+ return "{DOWN}";
+ case Key.Left:
+ return "{LEFT}";
+ case Key.Right:
+ return "{RIGHT}";
+ case Key.F1:
+ return "{F1}";
+ case Key.F2:
+ return "{F2}";
+ case Key.F3:
+ return "{F3}";
+ case Key.F4:
+ return "{F4}";
+ case Key.F5:
+ return "{F5}";
+ case Key.F6:
+ return "{F6}";
+ case Key.F7:
+ return "{F7}";
+ case Key.F8:
+ return "{F8}";
+ case Key.F9:
+ return "{F9}";
+ case Key.F10:
+ return "{F10}";
+ case Key.F11:
+ return "{F11}";
+ case Key.F12:
+ return "{F12}";
+ case Key.A:
+ return "A";
+ case Key.B:
+ return "B";
+ case Key.C:
+ return "C";
+ case Key.D:
+ return "D";
+ case Key.E:
+ return "E";
+ case Key.F:
+ return "F";
+ case Key.G:
+ return "G";
+ case Key.H:
+ return "H";
+ case Key.I:
+ return "I";
+ case Key.J:
+ return "J";
+ case Key.K:
+ return "K";
+ case Key.L:
+ return "L";
+ case Key.M:
+ return "M";
+ case Key.N:
+ return "N";
+ case Key.O:
+ return "O";
+ case Key.P:
+ return "P";
+ case Key.Q:
+ return "Q";
+ case Key.R:
+ return "R";
+ case Key.S:
+ return "S";
+ case Key.T:
+ return "T";
+ case Key.U:
+ return "U";
+ case Key.V:
+ return "V";
+ case Key.W:
+ return "W";
+ case Key.X:
+ return "X";
+ case Key.Y:
+ return "Y";
+ case Key.Z:
+ return "Z";
+ case Key.Num0:
+ return "0";
+ case Key.Num1:
+ return "1";
+ case Key.Num2:
+ return "2";
+ case Key.Num3:
+ return "3";
+ case Key.Num4:
+ return "4";
+ case Key.Num5:
+ return "5";
+ case Key.Num6:
+ return "6";
+ case Key.Num7:
+ return "7";
+ case Key.Num8:
+ return "8";
+ case Key.Num9:
+ return "9";
+ default:
+ return string.Empty;
+ }
+ }
+
+ ///
+ /// Sends a combination of keys, including the Windows key, to the system.
+ ///
+ /// The keys to send.
+ private static void SendWinKeyCombination(string keys)
+ {
+ bool winKeyDown = false;
+
+ if (keys.Contains("{WIN}"))
+ {
+ keybd_event(VK_LWIN, 0, KEYEVENTF_KEYDOWN, UIntPtr.Zero);
+ winKeyDown = true;
+ keys = keys.Replace("{WIN}", string.Empty); // Remove {WIN} from the string
+ }
+
+ System.Windows.Forms.SendKeys.SendWait(keys);
+
+ // Release Windows key
+ if (winKeyDown)
+ {
+ keybd_event(VK_LWIN, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
+ }
+ }
+ }
+}
diff --git a/src/common/UITestAutomation/ModuleConfigData.cs b/src/common/UITestAutomation/ModuleConfigData.cs
index ce04c13429..a64ece56f7 100644
--- a/src/common/UITestAutomation/ModuleConfigData.cs
+++ b/src/common/UITestAutomation/ModuleConfigData.cs
@@ -32,6 +32,47 @@ namespace Microsoft.PowerToys.UITest
Runner,
}
+ ///
+ /// Represents the window size for the UI test.
+ ///
+ public enum WindowSize
+ {
+ ///
+ /// Unspecified window size, won't make any size change
+ ///
+ UnSpecified,
+
+ ///
+ /// Small window size, 640 * 480
+ ///
+ Small,
+
+ ///
+ /// Small window size, 480 * 640
+ ///
+ Small_Vertical,
+
+ ///
+ /// Medium window size, 1024 * 768
+ ///
+ Medium,
+
+ ///
+ /// Medium window size, 768 * 1024
+ ///
+ Medium_Vertical,
+
+ ///
+ /// Large window size, 1920 * 1080
+ ///
+ Large,
+
+ ///
+ /// Large window size, 1080 * 1920
+ ///
+ Large_Vertical,
+ }
+
internal class ModuleConfigData
{
private Dictionary ModulePath { get; }
diff --git a/src/common/UITestAutomation/MouseHelper.cs b/src/common/UITestAutomation/MouseHelper.cs
new file mode 100644
index 0000000000..8215ea6790
--- /dev/null
+++ b/src/common/UITestAutomation/MouseHelper.cs
@@ -0,0 +1,49 @@
+// 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.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.UITest
+{
+ internal static class MouseHelper
+ {
+ [StructLayout(LayoutKind.Sequential)]
+ public struct POINT
+ {
+ public int X;
+ public int Y;
+ }
+
+ [DllImport("user32.dll")]
+ public static extern bool GetCursorPos(out POINT lpPoint);
+
+ [DllImport("user32.dll")]
+ public static extern bool SetCursorPos(int x, int y);
+
+ ///
+ /// Gets the current position of the mouse cursor as a tuple.
+ ///
+ /// A tuple containing the X and Y coordinates of the cursor.
+ public static Tuple GetMousePosition()
+ {
+ GetCursorPos(out POINT point);
+ return Tuple.Create(point.X, point.Y);
+ }
+
+ ///
+ /// Moves the mouse cursor to the specified screen coordinates.
+ ///
+ /// The new x-coordinate of the cursor.
+ /// The new y-coordinate of the cursor.
+ /// Provides methods for capturing the screen with the mouse cursor.
+ ///
+ internal static class ScreenCapture
+ {
+ [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 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);
+
+ ///
+ /// Represents a point with X and Y coordinates.
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct POINT
+ {
+ public int X;
+ public int Y;
+ }
+
+ ///
+ /// Contains information about the cursor.
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct CURSORINFO
+ {
+ ///
+ /// Gets or sets the size of the structure.
+ ///
+ public int CbSize;
+
+ ///
+ /// Gets or sets the cursor state.
+ ///
+ public int Flags;
+
+ ///
+ /// Gets or sets the handle to the cursor.
+ ///
+ public IntPtr HCursor;
+
+ ///
+ /// Gets or sets the screen position of the cursor.
+ ///
+ public POINT PTScreenPos;
+ }
+
+ private const int CURSORSHOWING = 0x00000001;
+ private const int DESKTOPHORZRES = 118;
+ private const int DESKTOPVERTRES = 117;
+ private const int DINORMAL = 0x0003;
+
+ ///
+ /// Captures the screen with the mouse cursor and saves it to the specified file path.
+ ///
+ /// The file path to save the captured image.
+ private static void CaptureScreenWithMouse(string filePath)
+ {
+ 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))
+ {
+ using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bitmap))
+ {
+ g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);
+
+ CURSORINFO cursorInfo;
+ cursorInfo.CbSize = Marshal.SizeOf();
+ if (GetCursorInfo(out cursorInfo) && cursorInfo.Flags == CURSORSHOWING)
+ {
+ using (System.Drawing.Graphics gIcon = System.Drawing.Graphics.FromImage(bitmap))
+ {
+ IntPtr hdcDest = gIcon.GetHdc();
+ DrawIconEx(hdcDest, cursorInfo.PTScreenPos.X, cursorInfo.PTScreenPos.Y, cursorInfo.HCursor, 0, 0, 0, IntPtr.Zero, DINORMAL);
+ gIcon.ReleaseHdc(hdcDest);
+ }
+ }
+ }
+
+ bitmap.Save(filePath, ImageFormat.Png);
+ }
+ }
+
+ ///
+ /// Captures a screenshot and saves it to the specified directory.
+ ///
+ /// The directory to save the screenshot.
+ private static void CaptureScreenshot(string directory)
+ {
+ string filePath = Path.Combine(directory, $"screenshot_{DateTime.Now:yyyyMMdd_HHmmssfff}.png");
+ CaptureScreenWithMouse(filePath);
+ }
+
+ ///
+ /// Timer callback method to capture a screenshot.
+ ///
+ /// The state object passed to the callback method.
+ public static void TimerCallback(object? state)
+ {
+ string directory = (string)state!;
+ CaptureScreenshot(directory);
+ }
+ }
+}
diff --git a/src/common/UITestAutomation/Session.cs b/src/common/UITestAutomation/Session.cs
index 9e5101fc75..d46d836c05 100644
--- a/src/common/UITestAutomation/Session.cs
+++ b/src/common/UITestAutomation/Session.cs
@@ -4,7 +4,7 @@
using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
-using System.Xml.Linq;
+using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
@@ -17,27 +17,78 @@ namespace Microsoft.PowerToys.UITest
///
public class Session
{
- private WindowsDriver Root { get; set; }
+ public WindowsDriver Root { get; set; }
private WindowsDriver WindowsDriver { get; set; }
- [DllImport("user32.dll")]
- private static extern bool SetForegroundWindow(nint hWnd);
+ private const string AdministratorPrefix = "Administrator: ";
- public Session(WindowsDriver root, WindowsDriver windowsDriver)
+ private List windowHandlers = new List();
+
+ private Window? MainWindow { get; set; }
+
+ ///
+ /// Gets Main Window Handler
+ ///
+ public IntPtr MainWindowHandler { get; private set; }
+
+ ///
+ /// Gets the RunAsAdmin flag.
+ /// If true, the session is running as admin.
+ /// If false, the session is not running as admin.
+ /// If null, no information is available.
+ ///
+ public bool? IsElevated { get; private set; }
+
+ public Session(WindowsDriver root, WindowsDriver windowsDriver, PowerToysModule scope, WindowSize size)
{
+ this.MainWindowHandler = IntPtr.Zero;
this.Root = root;
this.WindowsDriver = windowsDriver;
+
+ // Attach to the scope & reset MainWindowHandler
+ this.Attach(scope, size);
}
///
- /// Finds an element by selector.
+ /// Cleans up the Session Exe.
+ ///
+ public void Cleanup()
+ {
+ /*
+ foreach (var windowHandle in this.windowHandlers)
+ {
+ if (windowHandle == IntPtr.Zero)
+ {
+ continue;
+ }
+
+ try
+ {
+ var process = Process.GetProcessById((int)windowHandle);
+ if (process != null && !process.HasExited)
+ {
+ process.Kill();
+ process.WaitForExit();
+ }
+ }
+ catch
+ {
+ }
+ }
+ */
+
+ windowHandlers.Clear();
+ }
+
+ ///
+ /// Finds an Element or its derived class by selector.
///
/// The class of the element, should be Element or its derived class.
/// The selector to find the element.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// The found element.
- public T Find(By by, int timeoutMS = 3000)
+ public T Find(By by, int timeoutMS = 5000)
where T : Element, new()
{
Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
@@ -45,7 +96,7 @@ namespace Microsoft.PowerToys.UITest
// leverage findAll to filter out mismatched elements
var collection = this.FindAll(by, timeoutMS);
- Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}");
+ Assert.IsTrue(collection.Count > 0, $"UI-Element({typeof(T).Name}) not found using selector: {by}");
return collection[0];
}
@@ -55,9 +106,9 @@ namespace Microsoft.PowerToys.UITest
///
/// The class of the element, should be Element or its derived class.
/// The name of the element.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// The found element.
- public T Find(string name, int timeoutMS = 3000)
+ public T Find(string name, int timeoutMS = 5000)
where T : Element, new()
{
return this.Find(By.Name(name), timeoutMS);
@@ -67,9 +118,9 @@ namespace Microsoft.PowerToys.UITest
/// Shortcut for this.Find(by, timeoutMS)
///
/// The selector to find the element.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// The found element.
- public Element Find(By by, int timeoutMS = 3000)
+ public Element Find(By by, int timeoutMS = 5000)
{
return this.Find(by, timeoutMS);
}
@@ -78,21 +129,117 @@ namespace Microsoft.PowerToys.UITest
/// Shortcut for this.Find(By.Name(name), timeoutMS)
///
/// The name of the element.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// The found element.
- public Element Find(string name, int timeoutMS = 3000)
+ public Element Find(string name, int timeoutMS = 5000)
{
return this.Find(By.Name(name), timeoutMS);
}
///
- /// Finds all elements by selector.
+ /// Has only one Element or its derived class by selector.
+ ///
+ /// The class of the element, should be Element or its derived class.
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if only has one element, otherwise false.
+ public bool HasOne(By by, int timeoutMS = 5000)
+ where T : Element, new()
+ {
+ return this.FindAll(by, timeoutMS).Count == 1;
+ }
+
+ ///
+ /// Shortcut for this.HasOne(by, timeoutMS)
+ ///
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if only has one element, otherwise false.
+ public bool HasOne(By by, int timeoutMS = 5000)
+ {
+ return this.HasOne(by, timeoutMS);
+ }
+
+ ///
+ /// Shortcut for this.HasOne(By.Name(name), timeoutMS)
+ ///
+ /// The class of the element, should be Element or its derived class.
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if only has one element, otherwise false.
+ public bool HasOne(string name, int timeoutMS = 5000)
+ where T : Element, new()
+ {
+ return this.HasOne(By.Name(name), timeoutMS);
+ }
+
+ ///
+ /// Shortcut for this.HasOne(name, timeoutMS)
+ ///
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if only has one element, otherwise false.
+ public bool HasOne(string name, int timeoutMS = 5000)
+ {
+ return this.HasOne(By.Name(name), timeoutMS);
+ }
+
+ ///
+ /// Has one or more Element or its derived class by selector.
+ ///
+ /// The class of the element, should be Element or its derived class.
+ /// The selector to find the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if has one or more element, otherwise false.
+ public bool Has(By by, int timeoutMS = 5000)
+ where T : Element, new()
+ {
+ return this.FindAll(by, timeoutMS).Count >= 1;
+ }
+
+ ///
+ /// Shortcut for this.Has(by, timeoutMS)
+ ///
+ /// The selector to find the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if has one or more element, otherwise false.
+ public bool Has(By by, int timeoutMS = 5000)
+ {
+ return this.Has(by, timeoutMS);
+ }
+
+ ///
+ /// Shortcut for this.Has(By.Name(name), timeoutMS)
+ ///
+ /// The class of the element, should be Element or its derived class.
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if has one or more element, otherwise false.
+ public bool Has(string name, int timeoutMS = 5000)
+ where T : Element, new()
+ {
+ return this.Has(By.Name(name), timeoutMS);
+ }
+
+ ///
+ /// Shortcut for this.Has(name, timeoutMS)
+ ///
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if has one or more element, otherwise false.
+ public bool Has(string name, int timeoutMS = 5000)
+ {
+ return this.Has(name, timeoutMS);
+ }
+
+ ///
+ /// Finds all Element or its derived class by selector.
///
/// The class of the elements, should be Element or its derived class.
/// The selector to find the elements.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// A read-only collection of the found elements.
- public ReadOnlyCollection FindAll(By by, int timeoutMS = 3000)
+ public ReadOnlyCollection FindAll(By by, int timeoutMS = 5000)
where T : Element, new()
{
Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
@@ -122,9 +269,9 @@ namespace Microsoft.PowerToys.UITest
///
/// The class of the elements, should be Element or its derived class.
/// The name to find the elements.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// A read-only collection of the found elements.
- public ReadOnlyCollection FindAll(string name, int timeoutMS = 3000)
+ public ReadOnlyCollection FindAll(string name, int timeoutMS = 5000)
where T : Element, new()
{
return this.FindAll(By.Name(name), timeoutMS);
@@ -135,9 +282,9 @@ namespace Microsoft.PowerToys.UITest
/// Shortcut for this.FindAll(by, timeoutMS)
///
/// The selector to find the elements.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// A read-only collection of the found elements.
- public ReadOnlyCollection FindAll(By by, int timeoutMS = 3000)
+ public ReadOnlyCollection FindAll(By by, int timeoutMS = 5000)
{
return this.FindAll(by, timeoutMS);
}
@@ -147,55 +294,170 @@ namespace Microsoft.PowerToys.UITest
/// Shortcut for this.FindAll(By.Name(name), timeoutMS)
///
/// The name to find the elements.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// A read-only collection of the found elements.
- public ReadOnlyCollection FindAll(string name, int timeoutMS = 3000)
+ public ReadOnlyCollection FindAll(string name, int timeoutMS = 5000)
{
return this.FindAll(By.Name(name), timeoutMS);
}
///
- /// Keyboard Action key.
+ /// Sets the main window size.
///
- /// The Keys1 to click.
- /// The Keys2 to click.
- /// The Keys3 to click.
- /// The Keys4 to click.
- public void KeyboardAction(string key1, string key2 = "", string key3 = "", string key4 = "")
+ /// WindowSize enum
+ public void SetMainWindowSize(WindowSize size)
{
- PerformAction((actions, windowElement) =>
+ if (size == WindowSize.UnSpecified)
{
- if (string.IsNullOrEmpty(key2))
- {
- actions.SendKeys(key1);
- }
- else if (string.IsNullOrEmpty(key3))
- {
- actions.SendKeys(key1).SendKeys(key2);
- }
- else if (string.IsNullOrEmpty(key4))
- {
- actions.SendKeys(key1).SendKeys(key2).SendKeys(key3);
- }
- else
- {
- actions.SendKeys(key1).SendKeys(key2).SendKeys(key3).SendKeys(key4);
- }
+ return;
+ }
- actions.Release();
- actions.Build().Perform();
+ int width = 0, height = 0;
+
+ switch (size)
+ {
+ case WindowSize.Small:
+ width = 640;
+ height = 480;
+ break;
+ case WindowSize.Small_Vertical:
+ width = 480;
+ height = 640;
+ break;
+ case WindowSize.Medium:
+ width = 1024;
+ height = 768;
+ break;
+ case WindowSize.Medium_Vertical:
+ width = 768;
+ height = 1024;
+ break;
+ case WindowSize.Large:
+ width = 1920;
+ height = 1080;
+ break;
+ case WindowSize.Large_Vertical:
+ width = 1080;
+ height = 1920;
+ break;
+ }
+
+ if (width > 0 && height > 0)
+ {
+ this.SetMainWindowSize(width, height);
+ }
+ }
+
+ ///
+ /// Sets the main window size based on Width and Height.
+ ///
+ /// the width in pixel
+ /// the height in pixel
+ public void SetMainWindowSize(int width, int height)
+ {
+ if (this.MainWindowHandler == IntPtr.Zero
+ || width <= 0
+ || height <= 0)
+ {
+ return;
+ }
+
+ ApiHelper.SetWindowPos(this.MainWindowHandler, IntPtr.Zero, 0, 0, width, height, ApiHelper.SetWindowPosNoMove | ApiHelper.SetWindowPosNoZorder | ApiHelper.SetWindowPosShowWindow);
+
+ // Wait for 1000ms after resize
+ Task.Delay(1000).Wait();
+ }
+
+ ///
+ /// Close the main window.
+ ///
+ public void CloseMainWindow()
+ {
+ if (MainWindow != null)
+ {
+ MainWindow.Close();
+ MainWindow = null;
+ }
+ }
+
+ ///
+ /// Retrieves the color of the pixel at the specified screen coordinates.
+ ///
+ /// The X coordinate on the screen.
+ /// The Y coordinate on the screen.
+ /// The color of the pixel at the specified coordinates.
+ public Color GetPixelColor(int x, int y)
+ {
+ IntPtr hdc = ApiHelper.GetDC(IntPtr.Zero);
+ uint pixel = ApiHelper.GetPixel(hdc, x, y);
+ _ = ApiHelper.ReleaseDC(IntPtr.Zero, hdc);
+
+ int r = (int)(pixel & 0x000000FF);
+ int g = (int)((pixel & 0x0000FF00) >> 8);
+ int b = (int)((pixel & 0x00FF0000) >> 16);
+
+ return Color.FromArgb(r, g, b);
+ }
+
+ ///
+ /// Sends a combination of keys.
+ ///
+ /// The keys to send.
+ public void SendKeys(params Key[] keys)
+ {
+ PerformAction(() =>
+ {
+ KeyboardHelper.SendKeys(keys);
});
}
+ ///
+ /// Sends a sequence of keys.
+ ///
+ /// An array of keys to send.
+ public void SendKeySequence(params Key[] keys)
+ {
+ PerformAction(() =>
+ {
+ foreach (var key in keys)
+ {
+ KeyboardHelper.SendKeys(key);
+ }
+ });
+ }
+
+ ///
+ /// Gets the current position of the mouse cursor as a tuple.
+ ///
+ /// A tuple containing the X and Y coordinates of the cursor.
+ public Tuple GetMousePosition()
+ {
+ return MouseHelper.GetMousePosition();
+ }
+
+ ///
+ /// Moves the mouse cursor to the specified screen coordinates.
+ ///
+ /// The new x-coordinate of the cursor.
+ /// The new y-coordinate of the cursor.
+ {
+ MouseHelper.MoveMouseTo(x, y);
+ });
+ }
+
///
/// Attaches to an existing PowerToys module.
///
/// The PowerToys module to attach to.
+ /// The window size to set. Default is no change to window size
/// The attached session.
- public Session Attach(PowerToysModule module)
+ public Session Attach(PowerToysModule module, WindowSize size = WindowSize.UnSpecified)
{
string windowName = ModuleConfigData.Instance.GetModuleWindowName(module);
- return this.Attach(windowName);
+ return this.Attach(windowName, size);
}
///
@@ -203,26 +465,44 @@ namespace Microsoft.PowerToys.UITest
/// The session should be attached when a new app is started.
///
/// The window name to attach to.
+ /// The window size to set. Default is no change to window size
/// The attached session.
- public Session Attach(string windowName)
+ public Session Attach(string windowName, WindowSize size = WindowSize.UnSpecified)
{
+ this.IsElevated = null;
+ this.MainWindowHandler = IntPtr.Zero;
+
if (this.Root != null)
{
- var window = this.Root.FindElementByName(windowName);
- Assert.IsNotNull(window, $"Failed to attach. Window '{windowName}' not found");
+ // search window handler by window title (admin and non-admin titles)
+ var matchingWindows = ApiHelper.FindDesktopWindowHandler([windowName, AdministratorPrefix + windowName]);
+ if (matchingWindows.Count == 0 || matchingWindows[0].HWnd == IntPtr.Zero)
+ {
+ Assert.Fail($"Failed to attach. Window '{windowName}' not found");
+ }
+
+ // pick one from matching windows
+ this.MainWindowHandler = matchingWindows[0].HWnd;
+ this.IsElevated = matchingWindows[0].Title.StartsWith(AdministratorPrefix);
+
+ ApiHelper.SetForegroundWindow(this.MainWindowHandler);
+
+ var hexWindowHandle = this.MainWindowHandler.ToInt64().ToString("x");
- var windowHandle = new nint(int.Parse(window.GetAttribute("NativeWindowHandle")));
- SetForegroundWindow(windowHandle);
- var hexWindowHandle = windowHandle.ToString("x");
var appCapabilities = new AppiumOptions();
-
appCapabilities.AddAdditionalCapability("appTopLevelWindow", hexWindowHandle);
appCapabilities.AddAdditionalCapability("deviceName", "WindowsPC");
this.WindowsDriver = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), appCapabilities);
- Assert.IsNotNull(this.WindowsDriver, "Attach WindowsDriver is null");
- // Set implicit timeout to make element search retry every 500 ms
- this.WindowsDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3);
+ this.windowHandlers.Add(this.MainWindowHandler);
+
+ if (size != WindowSize.UnSpecified)
+ {
+ this.SetMainWindowSize(size);
+ }
+
+ // Set MainWindow
+ MainWindow = Find(matchingWindows[0].Title);
}
else
{
@@ -232,6 +512,71 @@ namespace Microsoft.PowerToys.UITest
return this;
}
+ private static class ApiHelper
+ {
+ [DllImport("user32.dll")]
+ public static extern bool SetForegroundWindow(IntPtr hWnd);
+
+ public const uint SetWindowPosNoMove = 0x0002;
+ public const uint SetWindowPosNoZorder = 0x0004;
+ public const uint SetWindowPosShowWindow = 0x0040;
+
+ [DllImport("user32.dll", SetLastError = true)]
+ public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
+
+ // Delegate for the EnumWindows callback function
+ private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
+
+ // P/Invoke declaration for EnumWindows
+ [DllImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
+
+ // P/Invoke declaration for GetWindowTextLength
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
+ private static extern int GetWindowTextLength(IntPtr hWnd);
+
+ // P/Invoke declaration for GetWindowText
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
+ private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
+
+ [DllImport("user32.dll")]
+ public static extern IntPtr GetDC(IntPtr hWnd);
+
+ [DllImport("gdi32.dll")]
+ public static extern uint GetPixel(IntPtr hdc, int x, int y);
+
+ [DllImport("user32.dll")]
+ public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
+
+ public static List<(IntPtr HWnd, string Title)> FindDesktopWindowHandler(string[] matchingWindowsTitles)
+ {
+ var windows = new List<(IntPtr HWnd, string Title)>();
+
+ _ = EnumWindows(
+ (hWnd, lParam) =>
+ {
+ int length = GetWindowTextLength(hWnd);
+ if (length > 0)
+ {
+ var builder = new StringBuilder(length + 1);
+ _ = GetWindowText(hWnd, builder, builder.Capacity);
+
+ var title = builder.ToString();
+ if (matchingWindowsTitles.Contains(title))
+ {
+ windows.Add((hWnd, title));
+ }
+ }
+
+ return true; // Continue enumeration
+ },
+ IntPtr.Zero);
+
+ return windows;
+ }
+ }
+
///
/// Simulates a manual operation on the element.
///
@@ -254,5 +599,26 @@ namespace Microsoft.PowerToys.UITest
Task.Delay(msPostAction).Wait();
}
}
+
+ ///
+ /// Simulates a manual operation on the element.
+ ///
+ /// The action to perform on the element.
+ /// The number of milliseconds to wait before the action. Default value is 500 ms
+ /// The number of milliseconds to wait after the action. Default value is 500 ms
+ protected void PerformAction(Action action, int msPreAction = 500, int msPostAction = 500)
+ {
+ if (msPreAction > 0)
+ {
+ Task.Delay(msPreAction).Wait();
+ }
+
+ action();
+
+ if (msPostAction > 0)
+ {
+ Task.Delay(msPostAction).Wait();
+ }
+ }
}
}
diff --git a/src/common/UITestAutomation/SessionHelper.cs b/src/common/UITestAutomation/SessionHelper.cs
index 3fa17a8e63..1ed58a4b6e 100644
--- a/src/common/UITestAutomation/SessionHelper.cs
+++ b/src/common/UITestAutomation/SessionHelper.cs
@@ -43,6 +43,7 @@ namespace Microsoft.PowerToys.UITest
Verb = "runas",
};
+ this.ExitExe(winAppDriverProcessInfo.FileName);
this.appDriver = Process.Start(winAppDriverProcessInfo);
var runnerProcessInfo = new ProcessStartInfo
@@ -56,9 +57,6 @@ namespace Microsoft.PowerToys.UITest
var desktopCapabilities = new AppiumOptions();
desktopCapabilities.AddAdditionalCapability("app", "Root");
this.Root = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), desktopCapabilities);
-
- // Set default timeout to 5 seconds
- this.Root.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
}
///
@@ -67,7 +65,8 @@ namespace Microsoft.PowerToys.UITest
/// The PowerToys module to start.
public SessionHelper Init()
{
- this.StartExe(locationPath + this.sessionPath);
+ this.ExitExe(this.locationPath + this.sessionPath);
+ this.StartExe(this.locationPath + this.sessionPath);
Assert.IsNotNull(this.Driver, $"Failed to initialize the test environment. Driver is null.");
@@ -94,29 +93,14 @@ namespace Microsoft.PowerToys.UITest
}
}
- ///
- /// Starts a new exe and takes control of it.
- ///
- /// The path to the application executable.
- public void StartExe(string appPath)
- {
- var opts = new AppiumOptions();
- opts.AddAdditionalCapability("app", appPath);
- Console.WriteLine($"appPath: {appPath}");
- this.Driver = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), opts);
-
- // Set default timeout to 5 seconds
- this.Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
- }
-
///
/// Exit a exe.
///
- /// The path to the application executable.
- public void ExitExe(string path)
+ /// The path to the application executable.
+ public void ExitExe(string appPath)
{
// Exit Exe
- string exeName = Path.GetFileNameWithoutExtension(path);
+ string exeName = Path.GetFileNameWithoutExtension(appPath);
// PowerToys.FancyZonesEditor
Process[] processes = Process.GetProcessesByName(exeName);
@@ -134,6 +118,21 @@ namespace Microsoft.PowerToys.UITest
}
}
+ ///
+ /// Starts a new exe and takes control of it.
+ ///
+ /// The path to the application executable.
+ public void StartExe(string appPath)
+ {
+ var opts = new AppiumOptions();
+ opts.AddAdditionalCapability("app", appPath);
+ opts.AddAdditionalCapability("ms:waitForAppLaunch", "5");
+ this.Driver = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), opts);
+
+ // Set default timeout to 5 seconds
+ this.Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
+ }
+
///
/// Exit now exe.
///
diff --git a/src/common/UITestAutomation/UITestAutomation.csproj b/src/common/UITestAutomation/UITestAutomation.csproj
index 0da17d85b8..09d1adeb6f 100644
--- a/src/common/UITestAutomation/UITestAutomation.csproj
+++ b/src/common/UITestAutomation/UITestAutomation.csproj
@@ -8,6 +8,9 @@
enabletruetrue
+ net9.0-windows10.0.22621.0
+ true
+ false
diff --git a/src/common/UITestAutomation/UITestBase.cs b/src/common/UITestAutomation/UITestBase.cs
index 40de0dc991..76606ec918 100644
--- a/src/common/UITestAutomation/UITestBase.cs
+++ b/src/common/UITestAutomation/UITestBase.cs
@@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Xml.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
@@ -17,19 +18,28 @@ namespace Microsoft.PowerToys.UITest
/// Base class that should be inherited by all Test Classes.
///
[TestClass]
- public class UITestBase
+ public class UITestBase : IDisposable
{
- public Session Session { get; set; }
+ public required TestContext TestContext { get; set; }
- private readonly SessionHelper sessionHelper;
+ public required Session Session { get; set; }
+ private readonly bool isInPipeline;
private readonly PowerToysModule scope;
+ private readonly WindowSize size;
+ private SessionHelper? sessionHelper;
+ private System.Threading.Timer? screenshotTimer;
+ private string? screenshotDirectory;
- public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings)
+ // private System.Threading.Timer? screenshotTimer;
+ // private string? screenshotDirectory;
+ public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified)
{
this.scope = scope;
+ this.size = size;
this.sessionHelper = new SessionHelper(scope).Init();
- this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver());
+ this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), scope, size);
+ this.isInPipeline = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("buildPlatforms"));
}
///
@@ -38,6 +48,24 @@ namespace Microsoft.PowerToys.UITest
[TestInitialize]
public void TestInit()
{
+ // Environment setup for pipeline:
+ // 1.Escape Popups
+ // 2.Continuous screenshots
+ if (isInPipeline)
+ {
+ 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));
+
+ // Escape Popups before starting
+ this.SendKeys(Key.Esc);
+ }
+
+ this.sessionHelper = new SessionHelper(scope).Init();
+ this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), scope, size);
+
if (this.scope == PowerToysModule.PowerToysSettings)
{
// close Debug warning dialog if any
@@ -50,12 +78,32 @@ namespace Microsoft.PowerToys.UITest
}
///
- /// UnInitializes the test.
+ /// Cleanups the test.
///
[TestCleanup]
- public void TestClean()
+ public void TestCleanup()
{
- this.sessionHelper.Cleanup();
+ if (isInPipeline)
+ {
+ screenshotTimer?.Change(Timeout.Infinite, Timeout.Infinite);
+ Dispose();
+ if (TestContext.CurrentTestOutcome is UnitTestOutcome.Failed
+ or UnitTestOutcome.Error
+ or UnitTestOutcome.Unknown)
+ {
+ Task.Delay(1000).Wait();
+ AddScreenShotsToTestResultsDirectory();
+ }
+ }
+
+ this.Session.Cleanup();
+ this.sessionHelper!.Cleanup();
+ }
+
+ public void Dispose()
+ {
+ screenshotTimer?.Dispose();
+ GC.SuppressFinalize(this);
}
///
@@ -64,22 +112,22 @@ namespace Microsoft.PowerToys.UITest
///
/// The class of the element, should be Element or its derived class.
/// The selector to find the element.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// The found element.
- protected T Find(By by, int timeoutMS = 3000)
+ protected T Find(By by, int timeoutMS = 5000)
where T : Element, new()
{
return this.Session.Find(by, timeoutMS);
}
///
- /// Shortcut for this.Session.Find(By.Name(name), timeoutMS)
+ /// Shortcut for this.Session.Find(name, timeoutMS)
///
/// The class of the element, should be Element or its derived class.
/// The name of the element.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// The found element.
- protected T Find(string name, int timeoutMS = 3000)
+ protected T Find(string name, int timeoutMS = 5000)
where T : Element, new()
{
return this.Session.Find(By.Name(name), timeoutMS);
@@ -89,33 +137,129 @@ namespace Microsoft.PowerToys.UITest
/// Shortcut for this.Session.Find(by, timeoutMS)
///
/// The selector to find the element.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// The found element.
- protected Element Find(By by, int timeoutMS = 3000)
+ protected Element Find(By by, int timeoutMS = 5000)
{
return this.Session.Find(by, timeoutMS);
}
///
- /// Shortcut for this.Session.Find(By.Name(name), timeoutMS)
+ /// Shortcut for this.Session.Find(name, timeoutMS)
///
/// The name of the element.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// The found element.
- protected Element Find(string name, int timeoutMS = 3000)
+ protected Element Find(string name, int timeoutMS = 5000)
{
return this.Session.Find(name, timeoutMS);
}
+ ///
+ /// Has only one Element or its derived class by selector.
+ ///
+ /// The class of the element, should be Element or its derived class.
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if only has one element, otherwise false.
+ public bool HasOne(By by, int timeoutMS = 5000)
+ where T : Element, new()
+ {
+ return this.FindAll(by, timeoutMS).Count == 1;
+ }
+
+ ///
+ /// Shortcut for this.Session.HasOne(by, timeoutMS)
+ ///
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if only has one element, otherwise false.
+ public bool HasOne(By by, int timeoutMS = 5000)
+ {
+ return this.Session.HasOne(by, timeoutMS);
+ }
+
+ ///
+ /// Shortcut for this.Session.HasOne(name, timeoutMS)
+ ///
+ /// The class of the element, should be Element or its derived class.
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if only has one element, otherwise false.
+ public bool HasOne(string name, int timeoutMS = 5000)
+ where T : Element, new()
+ {
+ return this.Session.HasOne(By.Name(name), timeoutMS);
+ }
+
+ ///
+ /// Shortcut for this.Session.HasOne(name, timeoutMS)
+ ///
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if only has one element, otherwise false.
+ public bool HasOne(string name, int timeoutMS = 5000)
+ {
+ return this.Session.HasOne(name, timeoutMS);
+ }
+
+ ///
+ /// Shortcut for this.Session.Has(by, timeoutMS)
+ ///
+ /// The class of the element, should be Element or its derived class.
+ /// The selector to find the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if has one or more element, otherwise false.
+ public bool Has(By by, int timeoutMS = 5000)
+ where T : Element, new()
+ {
+ return this.Session.FindAll(by, timeoutMS).Count >= 1;
+ }
+
+ ///
+ /// Shortcut for this.Session.Has(by, timeoutMS)
+ ///
+ /// The selector to find the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if has one or more element, otherwise false.
+ public bool Has(By by, int timeoutMS = 5000)
+ {
+ return this.Session.Has(by, timeoutMS);
+ }
+
+ ///
+ /// Shortcut for this.Session.Has(By.Name(name), timeoutMS)
+ ///
+ /// The class of the element, should be Element or its derived class.
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if has one or more element, otherwise false.
+ public bool Has(string name, int timeoutMS = 5000)
+ where T : Element, new()
+ {
+ return this.Session.Has(By.Name(name), timeoutMS);
+ }
+
+ ///
+ /// Shortcut for this.Session.Has(name, timeoutMS)
+ ///
+ /// The name of the element.
+ /// The timeout in milliseconds (default is 5000).
+ /// True if has one or more element, otherwise false.
+ public bool Has(string name, int timeoutMS = 5000)
+ {
+ return this.Session.Has(name, timeoutMS);
+ }
+
///
/// Finds all elements by selector.
/// Shortcut for this.Session.FindAll(by, timeoutMS)
///
/// The class of the elements, should be Element or its derived class.
/// The selector to find the elements.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// A read-only collection of the found elements.
- protected ReadOnlyCollection FindAll(By by, int timeoutMS = 3000)
+ protected ReadOnlyCollection FindAll(By by, int timeoutMS = 5000)
where T : Element, new()
{
return this.Session.FindAll(by, timeoutMS);
@@ -127,9 +271,9 @@ namespace Microsoft.PowerToys.UITest
///
/// The class of the elements, should be Element or its derived class.
/// The name of the elements.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// A read-only collection of the found elements.
- protected ReadOnlyCollection FindAll(string name, int timeoutMS = 3000)
+ protected ReadOnlyCollection FindAll(string name, int timeoutMS = 5000)
where T : Element, new()
{
return this.Session.FindAll(By.Name(name), timeoutMS);
@@ -140,9 +284,9 @@ namespace Microsoft.PowerToys.UITest
/// Shortcut for this.Session.FindAll(by, timeoutMS)
///
/// The selector to find the elements.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// A read-only collection of the found elements.
- protected ReadOnlyCollection FindAll(By by, int timeoutMS = 3000)
+ protected ReadOnlyCollection FindAll(By by, int timeoutMS = 5000)
{
return this.Session.FindAll(by, timeoutMS);
}
@@ -152,20 +296,94 @@ namespace Microsoft.PowerToys.UITest
/// Shortcut for this.Session.FindAll(By.Name(name), timeoutMS)
///
/// The name of the elements.
- /// The timeout in milliseconds (default is 3000).
+ /// The timeout in milliseconds (default is 5000).
/// A read-only collection of the found elements.
- protected ReadOnlyCollection FindAll(string name, int timeoutMS = 3000)
+ protected ReadOnlyCollection FindAll(string name, int timeoutMS = 5000)
{
return this.Session.FindAll(By.Name(name), timeoutMS);
}
+ ///
+ /// Captures the last screenshot when the test fails.
+ ///
+ protected void CaptureLastScreenshot()
+ {
+ // Implement your screenshot capture logic here
+ // For example, save a screenshot to a file and return the file path
+ string screenshotPath = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "last_screenshot.png");
+
+ this.Session.Root.GetScreenshot().SaveAsFile(screenshotPath, ScreenshotImageFormat.Png);
+
+ // Save screenshot to screenshotPath & upload to test attachment
+ this.TestContext.AddResultFile(screenshotPath);
+ }
+
+ ///
+ /// Retrieves the color of the pixel at the specified screen coordinates.
+ ///
+ /// The X coordinate on the screen.
+ /// The Y coordinate on the screen.
+ /// The color of the pixel at the specified coordinates.
+ public Color GetPixelColor(int x, int y)
+ {
+ return this.Session.GetPixelColor(x, y);
+ }
+
+ ///
+ /// Sends a combination of keys.
+ ///
+ /// The keys to send.
+ public void SendKeys(params Key[] keys)
+ {
+ this.Session.SendKeys(keys);
+ }
+
+ ///
+ /// Sends a sequence of keys.
+ ///
+ /// An array of keys to send.
+ public void SendKeySequence(params Key[] keys)
+ {
+ this.Session.SendKeySequence(keys);
+ }
+
+ ///
+ /// Gets the current position of the mouse cursor as a tuple.
+ ///
+ /// A tuple containing the X and Y coordinates of the cursor.
+ public Tuple GetMousePosition()
+ {
+ return this.Session.GetMousePosition();
+ }
+
+ ///
+ /// Moves the mouse cursor to the specified screen coordinates.
+ ///
+ /// The new x-coordinate of the cursor.
+ /// The new y-coordinate of the cursor.
/// Restart scope exe.
///
public void RestartScopeExe()
{
- this.sessionHelper.RestartScopeExe();
- this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver());
+ this.sessionHelper!.RestartScopeExe();
+ this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), this.scope, this.size);
return;
}
@@ -174,7 +392,7 @@ namespace Microsoft.PowerToys.UITest
///
public void ExitScopeExe()
{
- this.sessionHelper.ExitScopeExe();
+ this.sessionHelper!.ExitScopeExe();
return;
}
}
diff --git a/src/common/UITestAutomation/VisualAssert.cs b/src/common/UITestAutomation/VisualAssert.cs
new file mode 100644
index 0000000000..280740daca
--- /dev/null
+++ b/src/common/UITestAutomation/VisualAssert.cs
@@ -0,0 +1,144 @@
+// 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.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Drawing;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.PowerToys.UITest
+{
+ public static class VisualAssert
+ {
+ ///
+ /// Asserts current visual state of the element is equal with base line image.
+ /// To use this VisualAssert, you need to set Window Theme to Light-Mode to avoid Theme color difference in baseline image.
+ /// Such limiation could be removed either Auto-generate baseline image for both Light & Dark mode
+ ///
+ /// TestContext object
+ /// Element object
+ /// additional scenario name if two or more scenarios in one test
+ [RequiresUnreferencedCode("This method uses reflection which may not be compatible with trimming.")]
+ public static void AreEqual(TestContext? testContext, Element element, System.Reflection.Assembly callerAssembly, string scenarioSubname = "")
+ {
+ if (element == null)
+ {
+ Assert.Fail("Element object is null or invalid");
+ }
+
+ var stackTrace = new StackTrace();
+ var callerFrame = stackTrace.GetFrame(1);
+ var callerMethod = callerFrame?.GetMethod();
+
+ var callerName = callerMethod?.Name;
+ var callerClassName = callerMethod?.DeclaringType?.Name;
+
+ if (string.IsNullOrEmpty(callerName) || string.IsNullOrEmpty(callerClassName))
+ {
+ Assert.Fail("Unable to determine the caller method and class name.");
+ }
+
+ if (string.IsNullOrWhiteSpace(scenarioSubname))
+ {
+ scenarioSubname = string.Join("_", callerClassName, callerName);
+ }
+ else
+ {
+ scenarioSubname = string.Join("_", callerClassName, callerName, scenarioSubname.Trim());
+ }
+
+ var baselineImageResourceName = callerMethod!.DeclaringType!.Assembly.GetManifestResourceNames().Where(name => name.Contains(scenarioSubname)).FirstOrDefault();
+
+ var tempTestImagePath = GetTempFilePath(scenarioSubname, "test", ".png");
+
+ // Save the image with the user preference color erased
+ element.SaveToPngFile(tempTestImagePath, true);
+
+ if (string.IsNullOrEmpty(baselineImageResourceName)
+ || !Path.GetFileNameWithoutExtension(baselineImageResourceName).EndsWith(scenarioSubname))
+ {
+ Assert.Fail($"Baseline image for scenario {scenarioSubname} can not be found, test image saved in file://{tempTestImagePath.Replace('\\', '/')}");
+ }
+
+ var tempBaselineImagePath = GetTempFilePath(scenarioSubname, "baseline", Path.GetExtension(baselineImageResourceName));
+
+ bool isSame = false;
+
+#pragma warning disable CS8604 // Possible null reference argument.
+ using (var baselineImage = new Bitmap(callerMethod!.DeclaringType!.Assembly.GetManifestResourceStream(baselineImageResourceName)))
+ {
+ using (var testImage = new Bitmap(tempTestImagePath))
+ {
+ isSame = VisualAssert.AreEqual(baselineImage, testImage);
+
+ if (!isSame)
+ {
+ // Copy baseline image to temp folder as well
+ baselineImage.Save(tempBaselineImagePath);
+ }
+ }
+ }
+#pragma warning restore CS8604 // Possible null reference argument.
+
+ if (!isSame)
+ {
+ if (testContext != null)
+ {
+ testContext.AddResultFile(tempBaselineImagePath);
+ testContext.AddResultFile(tempTestImagePath);
+ }
+
+ Assert.Fail($"Fail to validate visual result for scenario {scenarioSubname}, baseline image can be found file://{tempBaselineImagePath.Replace('\\', '/')}, and test image can be found file://{tempTestImagePath.Replace('\\', '/')}");
+ }
+ }
+
+ ///
+ /// Get temp file path
+ ///
+ /// scenario name
+ /// baseline or test image
+ /// image file extension
+ /// full temp file path
+ private static string GetTempFilePath(string scenario, string imageType, string extension)
+ {
+ var tempFileFullName = $"{scenario}_{imageType}{extension}";
+
+ // Remove invalid filename character if any
+ Path.GetInvalidFileNameChars().ToList().ForEach(c => tempFileFullName = tempFileFullName.Replace(c, '-'));
+
+ return Path.Combine(Path.GetTempPath(), tempFileFullName);
+ }
+
+ ///
+ /// Test if two images are equal bit-by-bit
+ ///
+ /// baseline image
+ /// test image
+ /// true if are equal,otherwise false
+ private static bool AreEqual(Bitmap baselineImage, Bitmap testImage)
+ {
+ if (baselineImage.Width != testImage.Width || baselineImage.Height != testImage.Height)
+ {
+ return false;
+ }
+
+ // WinAppDriver sometimes adds a border to the screenshot (around 2 pix width), and it is not always consistent.
+ // So we exclude the border when comparing the images, and usually it is the edge of the windows, won't affect the comparison.
+ int excludeBorderWidth = 5, excludeBorderHeight = 5;
+
+ for (int x = excludeBorderWidth; x < baselineImage.Width - excludeBorderWidth; x++)
+ {
+ for (int y = excludeBorderHeight; y < baselineImage.Height - excludeBorderHeight; y++)
+ {
+ if (!VisualHelper.PixIsSame(baselineImage.GetPixel(x, y), testImage.GetPixel(x, y)))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/common/UITestAutomation/VisualHelper.cs b/src/common/UITestAutomation/VisualHelper.cs
new file mode 100644
index 0000000000..767ce29d97
--- /dev/null
+++ b/src/common/UITestAutomation/VisualHelper.cs
@@ -0,0 +1,170 @@
+// 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.Drawing;
+using System.Drawing.Imaging;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Microsoft.PowerToys.UITest
+{
+ internal static class VisualHelper
+ {
+ #pragma warning disable SA1307
+ [StructLayout(LayoutKind.Sequential)]
+ private struct IMMERSIVE_COLOR_PREFERENCE
+ {
+ public uint dwColorSetIndex;
+ public uint crStartColor;
+ public uint crAccentColor;
+ }
+ #pragma warning restore SA1307
+
+ [DllImport("uxtheme.dll", EntryPoint = "#120")]
+ private static extern IntPtr GetUserColorPreference(ref IMMERSIVE_COLOR_PREFERENCE pcpPreference, bool fForceReload);
+
+ ///
+ /// Gets the system accent color.
+ ///
+ /// The system accent color as a Color object.
+ private static Color GetSystemAccentColor()
+ {
+ IMMERSIVE_COLOR_PREFERENCE colorPreference = default(IMMERSIVE_COLOR_PREFERENCE);
+ GetUserColorPreference(ref colorPreference, true);
+ return ToColor(colorPreference.crStartColor);
+ }
+
+ ///
+ /// Converts a color value to a Color object.
+ ///
+ /// The color value.
+ /// The Color object.
+ private static Color ToColor(uint c)
+ {
+ int r = (int)(c & 0xFF) % 256;
+ int g = (int)((c >> 8) & 0xFF) % 256;
+ int b = (int)(c >> 16) % 256;
+ return Color.FromArgb(r, g, b);
+ }
+
+ ///
+ /// Gets HSL values from a Color object.
+ ///
+ /// The Color object.
+ /// A tuple containing the HSL values.
+ private static (double H, double S, double L) GetHSL(Color color)
+ {
+ double rNorm = color.R / 255.0;
+ double gNorm = color.G / 255.0;
+ double bNorm = color.B / 255.0;
+
+ double max = Math.Max(rNorm, Math.Max(gNorm, bNorm));
+ double min = Math.Min(rNorm, Math.Min(gNorm, bNorm));
+ double h = 0, s = 0, l = (max + min) / 2;
+
+ if (max != min)
+ {
+ double delta = max - min;
+ s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min);
+
+ if (max == rNorm)
+ {
+ h = ((gNorm - bNorm) / delta) + (gNorm < bNorm ? 6 : 0);
+ }
+ else if (max == gNorm)
+ {
+ h = ((bNorm - rNorm) / delta) + 2;
+ }
+ else if (max == bNorm)
+ {
+ h = ((rNorm - gNorm) / delta) + 4;
+ }
+
+ h /= 6;
+ }
+
+ return (h * 360, s * 100, l * 100);
+ }
+
+ ///
+ /// Makes a specific color in an image transparent.
+ ///
+ /// The path to the image file.
+ /// The path to save the output image file.
+ /// The target color to make transparent.
+ /// The fuzz factor for color comparison, default is 2.
+ private static void MakeColorTransparent(string imagePath, string outputPath, Color targetColor, int fuzz = 2)
+ {
+ var hsl = GetHSL(targetColor);
+
+ // Assert.IsNotNull(null, $"Target Color - H: {hsl.H}, S: {hsl.S}, L: {hsl.L}");
+ using (Bitmap originalBitmap = new Bitmap(imagePath))
+ {
+ using (Bitmap bitmap = new Bitmap(originalBitmap))
+ {
+ for (int y = 0; y < bitmap.Height; y++)
+ {
+ for (int x = 0; x < bitmap.Width; x++)
+ {
+ Color pixelColor = bitmap.GetPixel(x, y);
+ if (HueIsSame(pixelColor, targetColor, fuzz))
+ {
+ bitmap.SetPixel(x, y, Color.Transparent);
+ }
+ }
+ }
+
+ bitmap.Save(outputPath, ImageFormat.Png);
+ }
+ }
+ }
+
+ ///
+ /// Erases the user preference color from an image. Will overwrite this image.
+ ///
+ /// The path to the image file.
+ /// The fuzz factor for color comparison, default is 2.
+ public static void EraseUserPreferenceColor(string imagePath, int fuzz = 2)
+ {
+ Color systemColor = GetSystemAccentColor();
+ string tempPath = Path.GetTempFileName();
+ MakeColorTransparent(imagePath, tempPath, systemColor, fuzz);
+ File.Delete(imagePath);
+ File.Move(tempPath, imagePath);
+ }
+
+ ///
+ /// Compare two pixels with a fuzz factor
+ ///
+ /// base color
+ /// test color
+ /// fuzz factor, default is 10
+ /// true if same, otherwise is false
+ public static bool PixIsSame(Color c1, Color c2, int fuzz = 10)
+ {
+ return Math.Abs(c1.A - c2.A) <= fuzz && Math.Abs(c1.R - c2.R) <= fuzz && Math.Abs(c1.G - c2.G) <= fuzz && Math.Abs(c1.B - c2.B) <= fuzz;
+ }
+
+ ///
+ /// Compares the hue of two colors with a fuzz factor.
+ ///
+ /// The first color.
+ /// The second color.
+ /// The fuzz factor, default is 2.
+ /// True if the hues are the same, otherwise false.
+ public static bool HueIsSame(Color c1, Color c2, int fuzz = 2)
+ {
+ var h1 = GetHSL(c1).H;
+ var h2 = GetHSL(c2).H;
+ return Math.Abs(h1 - h2) <= fuzz;
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts.UITests/Baseline/HostModuleTests_TestAddingEntry.png b/src/modules/Hosts/Hosts.UITests/Baseline/HostModuleTests_TestAddingEntry.png
new file mode 100644
index 0000000000..9f3774215c
Binary files /dev/null and b/src/modules/Hosts/Hosts.UITests/Baseline/HostModuleTests_TestAddingEntry.png differ
diff --git a/src/modules/Hosts/Hosts.UITests/Baseline/HostModuleTests_TestEmptyView_EmptyView.png b/src/modules/Hosts/Hosts.UITests/Baseline/HostModuleTests_TestEmptyView_EmptyView.png
new file mode 100644
index 0000000000..bf7b51fdc1
Binary files /dev/null and b/src/modules/Hosts/Hosts.UITests/Baseline/HostModuleTests_TestEmptyView_EmptyView.png differ
diff --git a/src/modules/Hosts/Hosts.UITests/Baseline/HostModuleTests_TestEmptyView_NonEmptyView.png b/src/modules/Hosts/Hosts.UITests/Baseline/HostModuleTests_TestEmptyView_NonEmptyView.png
new file mode 100644
index 0000000000..ed85e4c31c
Binary files /dev/null and b/src/modules/Hosts/Hosts.UITests/Baseline/HostModuleTests_TestEmptyView_NonEmptyView.png differ
diff --git a/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs b/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs
index c50bbe988e..fd38283581 100644
--- a/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs
+++ b/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs
@@ -13,7 +13,7 @@ namespace Hosts.UITests
public class HostModuleTests : UITestBase
{
public HostModuleTests()
- : base(PowerToysModule.Hosts)
+ : base(PowerToysModule.Hosts, WindowSize.Small_Vertical)
{
}
@@ -31,14 +31,16 @@ namespace Hosts.UITests
///
///
///
- [TestMethod]
+ [TestMethod("Hosts.Basic.EmptyViewShouldWork")]
public void TestEmptyView()
{
this.CloseWarningDialog();
this.RemoveAllEntries();
// 'Add an entry' button (only show-up when list is empty) should be visible
- Assert.IsTrue(this.FindAll("Add an entry").Count == 1, "'Add an entry' button should be visible in the empty view");
+ Assert.IsTrue(this.HasOne("Add an entry"), "'Add an entry' button should be visible in the empty view");
+
+ // VisualAssert.AreEqual(this.Find("Entries"), "EmptyView");
// Click 'Add an entry' from empty-view for adding Host override rule
this.Find("Add an entry").Click();
@@ -46,8 +48,10 @@ namespace Hosts.UITests
this.AddEntry("192.168.0.1", "localhost", false, false);
// Should have one row now and not more empty view
- Assert.IsTrue(this.FindAll