diff --git a/PowerToys.sln b/PowerToys.sln
index 9b911b388be7..4484032f74c6 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -706,6 +706,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RegistryPreview.FuzzTests",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System", "src\modules\cmdpal\Exts\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj", "{64B88F02-CD88-4ED8-9624-989A800230F9}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MouseUtils.UITests", "src\modules\MouseUtils\MouseUtils.UITests\MouseUtils.UITests.csproj", "{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2574,14 +2576,18 @@ Global
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|ARM64.Build.0 = Debug|ARM64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x64.ActiveCfg = Debug|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x64.Build.0 = Debug|x64
- {64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x86.ActiveCfg = Debug|x64
- {64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x86.Build.0 = Debug|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|ARM64.ActiveCfg = Release|ARM64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|ARM64.Build.0 = Release|ARM64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x64.ActiveCfg = Release|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x64.Build.0 = Release|x64
- {64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x86.ActiveCfg = Release|x64
- {64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x86.Build.0 = Release|x64
+ {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|ARM64.Build.0 = Debug|ARM64
+ {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|x64.ActiveCfg = Debug|x64
+ {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|x64.Build.0 = Debug|x64
+ {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|ARM64.ActiveCfg = Release|ARM64
+ {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|ARM64.Build.0 = Release|ARM64
+ {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|x64.ActiveCfg = Release|x64
+ {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2852,6 +2858,7 @@ Global
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
{5702B3CC-8575-48D5-83D8-15BB42269CD3} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
{64B88F02-CD88-4ED8-9624-989A800230F9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
+ {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1} = {322566EF-20DC-43A6-B9F8-616AF942579A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/src/common/UITestAutomation/Element/ComboBox.cs b/src/common/UITestAutomation/Element/ComboBox.cs
new file mode 100644
index 000000000000..5462c339100f
--- /dev/null
+++ b/src/common/UITestAutomation/Element/ComboBox.cs
@@ -0,0 +1,28 @@
+// 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.
+
+namespace Microsoft.PowerToys.UITest
+{
+ public class ComboBox : Element
+ {
+ private static readonly string ExpectedControlType = "ControlType.ComboBox";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ComboBox()
+ {
+ this.TargetControlType = ComboBox.ExpectedControlType;
+ }
+
+ ///
+ /// Select the item of the ComboBox.
+ ///
+ /// The text to select from the list view.
+ public void Select(string value)
+ {
+ this.Find(value).Click();
+ }
+ }
+}
diff --git a/src/common/UITestAutomation/Element/Custom.cs b/src/common/UITestAutomation/Element/Custom.cs
new file mode 100644
index 000000000000..4875d44fa101
--- /dev/null
+++ b/src/common/UITestAutomation/Element/Custom.cs
@@ -0,0 +1,25 @@
+// 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.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.UITest
+{
+ public class Custom : Element
+ {
+ private static readonly string ExpectedControlType = "ControlType.Custom";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Custom()
+ {
+ this.TargetControlType = Custom.ExpectedControlType;
+ }
+ }
+}
diff --git a/src/common/UITestAutomation/Element/Element.cs b/src/common/UITestAutomation/Element/Element.cs
index 0ab23dba785c..144b6c082a83 100644
--- a/src/common/UITestAutomation/Element/Element.cs
+++ b/src/common/UITestAutomation/Element/Element.cs
@@ -113,9 +113,10 @@ public string ControlType
/// Click the UI element.
///
/// If true, performs a right-click; otherwise, performs a left-click. Default value is false
- public virtual void Click(bool rightClick = false)
+ public virtual void Click(bool rightClick = false, int msPreAction = 500, int msPostAction = 500)
{
- PerformAction((actions, windowElement) =>
+ PerformAction(
+ (actions, windowElement) =>
{
actions.MoveToElement(windowElement);
@@ -132,7 +133,9 @@ public virtual void Click(bool rightClick = false)
}
actions.Build().Perform();
- });
+ },
+ msPreAction,
+ msPostAction);
}
///
diff --git a/src/common/UITestAutomation/Element/Group.cs b/src/common/UITestAutomation/Element/Group.cs
new file mode 100644
index 000000000000..55619a281d6a
--- /dev/null
+++ b/src/common/UITestAutomation/Element/Group.cs
@@ -0,0 +1,25 @@
+// 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.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerToys.UITest
+{
+ public class Group : Element
+ {
+ private static readonly string ExpectedControlType = "ControlType.Group";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Group()
+ {
+ this.TargetControlType = Group.ExpectedControlType;
+ }
+ }
+}
diff --git a/src/common/UITestAutomation/Element/NavigationViewItem.cs b/src/common/UITestAutomation/Element/NavigationViewItem.cs
index 0a71d9a32100..3d1171208bf8 100644
--- a/src/common/UITestAutomation/Element/NavigationViewItem.cs
+++ b/src/common/UITestAutomation/Element/NavigationViewItem.cs
@@ -24,9 +24,12 @@ public NavigationViewItem()
/// Click the ListItem element.
///
/// If true, performs a right-click; otherwise, performs a left-click. Default value is false
- public override void Click(bool rightClick = false)
+ /// Pre action delay in milliseconds. Default value is 500
+ /// Post action delay in milliseconds. Default value is 500
+ public override void Click(bool rightClick = false, int msPreAction = 500, int msPostAction = 500)
{
- PerformAction((actions, windowElement) =>
+ PerformAction(
+ (actions, windowElement) =>
{
actions.MoveToElement(windowElement, 10, 10);
@@ -40,7 +43,9 @@ public override void Click(bool rightClick = false)
}
actions.Build().Perform();
- });
+ },
+ msPreAction,
+ msPostAction);
}
///
diff --git a/src/common/UITestAutomation/Element/Slider.cs b/src/common/UITestAutomation/Element/Slider.cs
new file mode 100644
index 000000000000..837b6bac594e
--- /dev/null
+++ b/src/common/UITestAutomation/Element/Slider.cs
@@ -0,0 +1,119 @@
+// 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 Microsoft.VisualStudio.TestTools.UnitTesting;
+using OpenQA.Selenium.Appium.Windows;
+
+namespace Microsoft.PowerToys.UITest
+{
+ public class Slider : Element
+ {
+ private static readonly string ExpectedControlType = "ControlType.Slider";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Slider()
+ {
+ this.TargetControlType = Slider.ExpectedControlType;
+ }
+
+ ///
+ /// Gets the value of a Slider (WindowsElement)
+ ///
+ /// The integer value of the slider
+ public int GetValue()
+ {
+ return this.Text == string.Empty ? 0 : int.Parse(this.Text);
+ }
+
+ ///
+ /// Sets the value of a Slider (WindowsElement) to the specified integer value.
+ /// Throws an exception if the value is out of the slider's valid range.
+ ///
+ /// The target integer value to set
+ public void SetValue(int targetValue)
+ {
+ // Read range and current value
+ int min = int.Parse(this.GetAttribute("RangeValue.Minimum"));
+ int max = int.Parse(this.GetAttribute("RangeValue.Maximum"));
+ int current = int.Parse(this.Text);
+
+ // Use Assert to check if the target value is within the valid range
+ Assert.IsTrue(
+ targetValue >= min && targetValue <= max,
+ $"Target value {targetValue} is out of range (min: {min}, max: {max}).");
+
+ // Compute difference
+ int diff = targetValue - current;
+ if (diff == 0)
+ {
+ return;
+ }
+
+ string key = diff > 0 ? OpenQA.Selenium.Keys.Right : OpenQA.Selenium.Keys.Left;
+ int steps = Math.Abs(diff);
+
+ for (int i = 0; i < steps; i++)
+ {
+ this.SendKeys(key);
+
+ // Thread.Sleep(2);
+ }
+
+ // Final check
+ int finalValue = int.Parse(this.Text);
+ Assert.AreEqual(
+ targetValue, finalValue, $"Slider value mismatch: expected {targetValue}, but got {finalValue}.");
+ }
+
+ ///
+ /// Sets the value of a Slider (WindowsElement) to the specified integer value.
+ /// Throws an exception if the value is out of the slider's valid range.
+ ///
+ /// The target integer value to set
+ public void QuickSetValue(int targetValue)
+ {
+ // Read range and current value
+ int min = int.Parse(this.GetAttribute("RangeValue.Minimum"));
+ int max = int.Parse(this.GetAttribute("RangeValue.Maximum"));
+ int current = int.Parse(this.Text);
+
+ // Use Assert to check if the target value is within the valid range
+ Assert.IsTrue(
+ targetValue >= min && targetValue <= max,
+ $"Target value {targetValue} is out of range (min: {min}, max: {max}).");
+
+ // Compute difference
+ int diff = targetValue - current;
+ if (diff == 0)
+ {
+ return;
+ }
+
+ string key = diff > 0 ? OpenQA.Selenium.Keys.Right : OpenQA.Selenium.Keys.Left;
+ int steps = Math.Abs(diff);
+
+ int maxKeysPerSend = 50;
+ int fullChunks = steps / maxKeysPerSend;
+ int remainder = steps % maxKeysPerSend;
+ for (int i = 0; i < fullChunks; i++)
+ {
+ SendKeys(new string(key[0], maxKeysPerSend));
+ Thread.Sleep(2);
+ }
+
+ if (remainder > 0)
+ {
+ SendKeys(new string(key[0], remainder));
+ Thread.Sleep(2);
+ }
+
+ // Final check
+ int finalValue = int.Parse(this.Text);
+ Assert.AreEqual(
+ targetValue, finalValue, $"Slider value mismatch: expected {targetValue}, but got {finalValue}.");
+ }
+ }
+}
diff --git a/src/common/UITestAutomation/KeyboardHelper.cs b/src/common/UITestAutomation/KeyboardHelper.cs
index 1892ab9dd304..d4833059abd2 100644
--- a/src/common/UITestAutomation/KeyboardHelper.cs
+++ b/src/common/UITestAutomation/KeyboardHelper.cs
@@ -17,6 +17,8 @@ namespace Microsoft.PowerToys.UITest
public enum Key
{
Ctrl,
+ LCtrl,
+ RCtrl,
Alt,
Shift,
Tab,
@@ -122,6 +124,12 @@ public static void ReleaseKey(Key key)
ReleaseVirtualKey(TranslateKeyHex(key));
}
+ public static void SendKey(Key key)
+ {
+ PressVirtualKey(TranslateKeyHex(key));
+ ReleaseVirtualKey(TranslateKeyHex(key));
+ }
+
///
/// Translates a key to its corresponding SendKeys representation.
///
@@ -133,6 +141,10 @@ private static string TranslateKey(Key key)
{
case Key.Ctrl:
return "^";
+ case Key.LCtrl:
+ return "^";
+ case Key.RCtrl:
+ return "^";
case Key.Alt:
return "%";
case Key.Shift:
@@ -285,6 +297,12 @@ private static byte TranslateKeyHex(Key key)
return 0x12; // Alt Key - 0x12 in hex
case Key.Shift:
return 0x10; // Shift Key - 0x10 in hex
+ case Key.LCtrl:
+ return 0xA2; // Left Ctrl Key - 0xA2 in hex
+ case Key.RCtrl: // Right Ctrl Key - 0xA3 in hex
+ return 0xA3;
+ case Key.A:
+ return 0x41; // A Key - 0x41 in hex
default:
throw new ArgumentException($"Key {key} is not supported, Please add your key at TranslateKeyHex for translation to hex.");
}
diff --git a/src/common/UITestAutomation/ModuleConfigData.cs b/src/common/UITestAutomation/ModuleConfigData.cs
index 1b97546a37ef..a64ece56f757 100644
--- a/src/common/UITestAutomation/ModuleConfigData.cs
+++ b/src/common/UITestAutomation/ModuleConfigData.cs
@@ -29,6 +29,7 @@ public enum PowerToysModule
PowerToysSettings,
FancyZone,
Hosts,
+ Runner,
}
///
@@ -93,6 +94,7 @@ private ModuleConfigData()
[PowerToysModule.PowerToysSettings] = "PowerToys Settings",
[PowerToysModule.FancyZone] = "FancyZones Layout",
[PowerToysModule.Hosts] = "Hosts File Editor",
+ [PowerToysModule.Runner] = "PowerToys",
};
// Exe start path for the module if it exists.
@@ -101,6 +103,7 @@ private ModuleConfigData()
[PowerToysModule.PowerToysSettings] = @"\..\..\..\WinUI3Apps\PowerToys.Settings.exe",
[PowerToysModule.FancyZone] = @"\..\..\..\PowerToys.FancyZonesEditor.exe",
[PowerToysModule.Hosts] = @"\..\..\..\WinUI3Apps\PowerToys.Hosts.exe",
+ [PowerToysModule.Runner] = @"\..\..\..\PowerToys.exe",
};
}
diff --git a/src/common/UITestAutomation/MouseHelper.cs b/src/common/UITestAutomation/MouseHelper.cs
index 8215ea679083..c08e1c61def7 100644
--- a/src/common/UITestAutomation/MouseHelper.cs
+++ b/src/common/UITestAutomation/MouseHelper.cs
@@ -11,6 +11,23 @@
namespace Microsoft.PowerToys.UITest
{
+ public enum MouseActionType
+ {
+ LeftClick,
+ RightClick,
+ MiddleClick,
+ LeftDoubleClick,
+ RightDoubleClick,
+ LeftDown,
+ LeftUp,
+ RightDown,
+ RightUp,
+ MiddleDown,
+ MiddleUp,
+ ScrollUp,
+ ScrollDown,
+ }
+
internal static class MouseHelper
{
[StructLayout(LayoutKind.Sequential)]
@@ -20,12 +37,29 @@ public struct POINT
public int Y;
}
+ [Flags]
+ internal enum MouseEvent
+ {
+ LeftDown = 0x0002,
+ LeftUp = 0x0004,
+ RightDown = 0x0008,
+ RightUp = 0x0010,
+ MiddleDown = 0x0020,
+ MiddleUp = 0x0040,
+ Wheel = 0x0800,
+ }
+
[DllImport("user32.dll")]
public static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll")]
public static extern bool SetCursorPos(int x, int y);
+ [DllImport("user32.dll")]
+#pragma warning disable SA1300 // Element should begin with upper-case letter
+ private static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, UIntPtr dwExtraInfo);
+#pragma warning restore SA1300 // Element should begin with upper-case letter
+
///
/// Gets the current position of the mouse cursor as a tuple.
///
@@ -45,5 +79,139 @@ public static void MoveMouseTo(int x, int y)
{
SetCursorPos(x, y);
}
+
+ ///
+ /// The delay in milliseconds between mouse down and up events to simulate a click.
+ ///
+ private const int ClickDelay = 100;
+
+ ///
+ /// The amount of scroll units to simulate a single mouse wheel tick.
+ ///
+ private const int ScrollAmount = 120;
+
+ ///
+ /// Simulates a left mouse click (press and release).
+ ///
+ public static void LeftClick()
+ {
+ LeftDown();
+ Thread.Sleep(ClickDelay);
+ LeftUp();
+ }
+
+ ///
+ /// Simulates a right mouse click (press and release).
+ ///
+ public static void RightClick()
+ {
+ RightDown();
+ Thread.Sleep(ClickDelay);
+ RightUp();
+ }
+
+ ///
+ /// Simulates a middle mouse click (press and release).
+ ///
+ public static void MiddleClick()
+ {
+ MiddleDown();
+ Thread.Sleep(ClickDelay);
+ MiddleUp();
+ }
+
+ ///
+ /// Simulates a left mouse double-click.
+ ///
+ public static void LeftDoubleClick()
+ {
+ LeftClick();
+ Thread.Sleep(ClickDelay);
+ LeftClick();
+ }
+
+ ///
+ /// Simulates a right mouse double-click.
+ ///
+ public static void RightDoubleClick()
+ {
+ RightClick();
+ Thread.Sleep(ClickDelay);
+ RightClick();
+ }
+
+ ///
+ /// Simulates pressing the left mouse button down.
+ ///
+ public static void LeftDown()
+ {
+ mouse_event((uint)MouseEvent.LeftDown, 0, 0, 0, UIntPtr.Zero);
+ }
+
+ ///
+ /// Simulates pressing the right mouse button down.
+ ///
+ public static void RightDown()
+ {
+ mouse_event((uint)MouseEvent.RightDown, 0, 0, 0, UIntPtr.Zero);
+ }
+
+ ///
+ /// Simulates pressing the middle mouse button down.
+ ///
+ public static void MiddleDown()
+ {
+ mouse_event((uint)MouseEvent.MiddleDown, 0, 0, 0, UIntPtr.Zero);
+ }
+
+ ///
+ /// Simulates releasing the left mouse button.
+ ///
+ public static void LeftUp()
+ {
+ mouse_event((uint)MouseEvent.LeftUp, 0, 0, 0, UIntPtr.Zero);
+ }
+
+ ///
+ /// Simulates releasing the right mouse button.
+ ///
+ public static void RightUp()
+ {
+ mouse_event((uint)MouseEvent.RightUp, 0, 0, 0, UIntPtr.Zero);
+ }
+
+ ///
+ /// Simulates releasing the middle mouse button.
+ ///
+ public static void MiddleUp()
+ {
+ mouse_event((uint)MouseEvent.MiddleUp, 0, 0, 0, UIntPtr.Zero);
+ }
+
+ ///
+ /// Simulates a mouse scroll wheel action by a specified amount.
+ /// Positive values scroll up, negative values scroll down.
+ ///
+ /// The scroll amount. Typically 120 or -120 per tick.
+ public static void ScrollWheel(int amount)
+ {
+ mouse_event((uint)MouseEvent.Wheel, 0, 0, (uint)amount, UIntPtr.Zero);
+ }
+
+ ///
+ /// Simulates scrolling the mouse wheel up by one tick.
+ ///
+ public static void ScrollUp()
+ {
+ ScrollWheel(ScrollAmount);
+ }
+
+ ///
+ /// Simulates scrolling the mouse wheel down by one tick.
+ ///
+ public static void ScrollDown()
+ {
+ ScrollWheel(-ScrollAmount);
+ }
}
}
diff --git a/src/common/UITestAutomation/Session.cs b/src/common/UITestAutomation/Session.cs
index c29c3fd92660..278ea1ff7001 100644
--- a/src/common/UITestAutomation/Session.cs
+++ b/src/common/UITestAutomation/Session.cs
@@ -362,7 +362,7 @@ public void SetMainWindowSize(int width, int height)
return;
}
- ApiHelper.SetWindowPos(this.MainWindowHandler, IntPtr.Zero, 0, 0, width, height, ApiHelper.SetWindowPosNoMove | ApiHelper.SetWindowPosNoZorder | ApiHelper.SetWindowPosShowWindow);
+ ApiHelper.SetWindowPos(this.MainWindowHandler, IntPtr.Zero, 0, 0, width, height, ApiHelper.SetWindowPosNoZorder | ApiHelper.SetWindowPosShowWindow);
// Wait for 1000ms after resize
Task.Delay(1000).Wait();
@@ -399,6 +399,18 @@ public Color GetPixelColor(int x, int y)
return Color.FromArgb(r, g, b);
}
+ ///
+ /// Retrieves the color of the pixel at the specified screen coordinates as a string.
+ ///
+ /// The X coordinate on the screen.
+ /// The Y coordinate on the screen.
+ /// The color of the pixel at the specified coordinates.
+ public string GetPixelColorString(int x, int y)
+ {
+ Color color = this.GetPixelColor(x, y);
+ return $"#{color.R:X2}{color.G:X2}{color.B:X2}";
+ }
+
///
/// Gets the size of the display.
///
@@ -451,6 +463,21 @@ public void ReleaseKey(Key key)
});
}
+ ///
+ /// press and hold the specified key.
+ ///
+ /// The key to press and release .
+ public void SendKey(Key key, int msPreAction = 500, int msPostAction = 500)
+ {
+ PerformAction(
+ () =>
+ {
+ KeyboardHelper.SendKey(key);
+ },
+ msPreAction,
+ msPostAction);
+ }
+
///
/// Sends a sequence of keys.
///
@@ -488,6 +515,66 @@ public void MoveMouseTo(int x, int y)
});
}
+ ///
+ /// Performs a mouse action based on the specified action type.
+ ///
+ /// The mouse action to perform.
+ /// Pre-action delay in milliseconds.
+ /// Post-action delay in milliseconds.
+ public void PerformMouseAction(MouseActionType action, int msPreAction = 500, int msPostAction = 500)
+ {
+ PerformAction(
+ () =>
+ {
+ switch (action)
+ {
+ case MouseActionType.LeftClick:
+ MouseHelper.LeftClick();
+ break;
+ case MouseActionType.RightClick:
+ MouseHelper.RightClick();
+ break;
+ case MouseActionType.MiddleClick:
+ MouseHelper.MiddleClick();
+ break;
+ case MouseActionType.LeftDoubleClick:
+ MouseHelper.LeftDoubleClick();
+ break;
+ case MouseActionType.RightDoubleClick:
+ MouseHelper.RightDoubleClick();
+ break;
+ case MouseActionType.LeftDown:
+ MouseHelper.LeftDown();
+ break;
+ case MouseActionType.LeftUp:
+ MouseHelper.LeftUp();
+ break;
+ case MouseActionType.RightDown:
+ MouseHelper.RightDown();
+ break;
+ case MouseActionType.RightUp:
+ MouseHelper.RightUp();
+ break;
+ case MouseActionType.MiddleDown:
+ MouseHelper.MiddleDown();
+ break;
+ case MouseActionType.MiddleUp:
+ MouseHelper.MiddleUp();
+ break;
+ case MouseActionType.ScrollUp:
+ MouseHelper.ScrollUp();
+ break;
+ case MouseActionType.ScrollDown:
+ MouseHelper.ScrollDown();
+ break;
+ default:
+ throw new ArgumentException("Unsupported mouse action.", nameof(action));
+ }
+ },
+ msPreAction,
+ msPostAction);
+ }
+
///
/// Attaches to an existing PowerToys module.
///
diff --git a/src/common/UITestAutomation/SessionHelper.cs b/src/common/UITestAutomation/SessionHelper.cs
index 92346863f8fa..49c74288df7a 100644
--- a/src/common/UITestAutomation/SessionHelper.cs
+++ b/src/common/UITestAutomation/SessionHelper.cs
@@ -20,6 +20,8 @@ internal class SessionHelper
// Default session path is PowerToys settings dashboard
private readonly string sessionPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.PowerToysSettings);
+ private readonly string runnerPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.Runner);
+
private string? locationPath;
private WindowsDriver Root { get; set; }
@@ -27,10 +29,14 @@ internal class SessionHelper
private WindowsDriver? Driver { get; set; }
private Process? appDriver;
+ private Process? runner;
+
+ private PowerToysModule scope;
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "")]
public SessionHelper(PowerToysModule scope)
{
+ this.scope = scope;
this.sessionPath = ModuleConfigData.Instance.GetModulePath(scope);
this.locationPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
@@ -43,6 +49,17 @@ public SessionHelper(PowerToysModule scope)
this.ExitExe(winAppDriverProcessInfo.FileName);
this.appDriver = Process.Start(winAppDriverProcessInfo);
+ var runnerProcessInfo = new ProcessStartInfo
+ {
+ FileName = locationPath + this.runnerPath,
+ Verb = "runas",
+ };
+
+ if (scope == PowerToysModule.PowerToysSettings)
+ {
+ this.runner = Process.Start(runnerProcessInfo);
+ }
+
var desktopCapabilities = new AppiumOptions();
desktopCapabilities.AddAdditionalCapability("app", "Root");
this.Root = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), desktopCapabilities);
@@ -72,6 +89,11 @@ public void Cleanup()
{
appDriver?.Kill();
appDriver?.WaitForExit(); // Optional: Wait for the process to exit
+ if (this.scope == PowerToysModule.PowerToysSettings)
+ {
+ runner?.Kill();
+ runner?.WaitForExit(); // Optional: Wait for the process to exit
+ }
}
catch (Exception ex)
{
diff --git a/src/modules/MouseUtils/MouseUtils.UITests/FindMyMouseTests.cs b/src/modules/MouseUtils/MouseUtils.UITests/FindMyMouseTests.cs
new file mode 100644
index 000000000000..ea326bcaf172
--- /dev/null
+++ b/src/modules/MouseUtils/MouseUtils.UITests/FindMyMouseTests.cs
@@ -0,0 +1,458 @@
+// 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.Globalization;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Microsoft.PowerToys.UITest;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Windows.Devices.Printers;
+using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window;
+
+namespace MouseUtils.UITests
+{
+ [TestClass]
+ public class FindMyMouseTests : UITestBase
+ {
+ ///
+ /// Test Warning Dialog at startup
+ ///
+ /// -
+ /// Validating Warning-Dialog will be shown if 'Show a warning at startup' toggle is On.
+ ///
+ /// -
+ /// Validating Warning-Dialog will NOT be shown if 'Show a warning at startup' toggle is Off.
+ ///
+ /// -
+ /// Validating click 'Quit' button in Warning-Dialog, the Hosts File Editor window would be closed.
+ ///
+ /// -
+ /// Validating click 'Accept' button in Warning-Dialog, the Hosts File Editor window would NOT be closed.
+ ///
+ ///
+ ///
+ [TestMethod]
+ public void TestEnableFindMyMouse()
+ {
+ LaunchFromSetting();
+
+ var settings = new FindMyMouseSettings();
+ settings.OverlayOpacity = "100";
+ settings.Radius = "50";
+ settings.InitialZoom = "1";
+ settings.AnimationDuration = "0";
+ settings.BackgroundColor = "000000";
+ settings.SpotlightColor = "FFFFFF";
+ var foundCustom = this.Find("Find My Mouse");
+ if (foundCustom != null)
+ {
+ foundCustom.Find("Enable Find My Mouse").Toggle(true);
+ CheckAnimationEnable(ref foundCustom);
+
+ // foundCustom.Find("Enable Find My Mouse").Toggle(false);
+ SetFindMyMouseActivationMethod(ref foundCustom, "Press Left Control twice");
+ Assert.IsNotNull(foundCustom, "Find My Mouse group not found.");
+ SetFindMyMouseAppearanceBehavior(ref foundCustom, ref settings);
+
+ var excludedApps = foundCustom.Find("Excluded apps");
+ if (excludedApps != null)
+ {
+ excludedApps.Click();
+ excludedApps.Click();
+ }
+ else
+ {
+ Assert.Fail("Activation method group not found.");
+ }
+ }
+ else
+ {
+ Assert.Fail("Find My Mouse group not found.");
+ }
+
+ // [Test Case]Enable FindMyMouse. Then, without moving your mouse: Press Left Ctrl twice and verify the overlay appears.
+ VerifySpotlightSettings(ref settings);
+
+ // [Test Case]Enable FindMyMouse. Then, without moving your mouse: Press any other key and verify the overlay disappears.
+ Session.SendKeys(Key.A);
+ VerifySpotlightDisappears(ref settings);
+
+ // [Test Case]Enable FindMyMouse. Then, without moving your mouse: Press Left Ctrl twice and verify the overlay appears.
+ VerifySpotlightSettings(ref settings);
+
+ // [Test Case]Enable FindMyMouse. Then, without moving your mouse: Press a mouse button and verify the overlay disappears.
+ Task.Delay(1000).Wait();
+
+ // MouseSimulator.LeftClick();
+ Session.PerformMouseAction(MouseActionType.LeftClick, 500, 1000);
+
+ VerifySpotlightDisappears(ref settings);
+ }
+
+ [TestMethod]
+ public void TestDisableFindMyMouse()
+ {
+ LaunchFromSetting();
+
+ var settings = new FindMyMouseSettings();
+ settings.OverlayOpacity = "100";
+ settings.Radius = "50";
+ settings.InitialZoom = "1";
+ settings.AnimationDuration = "0";
+ settings.BackgroundColor = "000000";
+ settings.SpotlightColor = "FFFFFF";
+ var foundCustom = this.Find("Find My Mouse");
+ if (foundCustom != null)
+ {
+ foundCustom.Find("Enable Find My Mouse").Toggle(true);
+
+ // foundCustom.Find("Enable Find My Mouse").Toggle(false);
+ SetFindMyMouseActivationMethod(ref foundCustom, "Press Left Control twice");
+ Assert.IsNotNull(foundCustom);
+ SetFindMyMouseAppearanceBehavior(ref foundCustom, ref settings);
+
+ var excludedApps = foundCustom.Find("Excluded apps");
+ if (excludedApps != null)
+ {
+ excludedApps.Click();
+ excludedApps.Click();
+ }
+ else
+ {
+ Assert.Fail("Activation method group not found.");
+ }
+ }
+ else
+ {
+ Assert.Fail("Find My Mouse group not found.");
+ }
+
+ // [Test Case]Enable FindMyMouse. Then, without moving your mouse: Press Left Ctrl twice and verify the overlay appears.
+ // VerifySpotlightSettings(ref settings);
+ ActivateSpotlight(ref settings);
+ VerifySpotlightAppears(ref settings);
+
+ // [Test Case] Disable FindMyMouse. Verify the overlay no longer appears when you press Left Ctrl twice
+ foundCustom.Find("Enable Find My Mouse").Toggle(false);
+ Task.Delay(1000).Wait();
+ ActivateSpotlight(ref settings);
+
+ VerifySpotlightDisappears(ref settings);
+
+ // [Test Case] Press Left Ctrl twice and verify the overlay appears
+ foundCustom.Find("Enable Find My Mouse").Toggle(true);
+ ActivateSpotlight(ref settings);
+ VerifySpotlightAppears(ref settings);
+
+ Session.PerformMouseAction(MouseActionType.LeftClick);
+ }
+
+ [TestMethod]
+ public void TestDisableFindMyMouse2()
+ {
+ LaunchFromSetting();
+
+ var settings = new FindMyMouseSettings();
+ settings.OverlayOpacity = "100";
+ settings.Radius = "50";
+ settings.InitialZoom = "1";
+ settings.AnimationDuration = "0";
+ settings.BackgroundColor = "000000";
+ settings.SpotlightColor = "FFFFFF";
+ var foundCustom = this.Find("Find My Mouse");
+ if (foundCustom != null)
+ {
+ foundCustom.Find("Enable Find My Mouse").Toggle(true);
+
+ // foundCustom.Find("Enable Find My Mouse").Toggle(false);
+ SetFindMyMouseActivationMethod(ref foundCustom, "Press Left Control twice");
+ Assert.IsNotNull(foundCustom, "Find My Mouse group not found.");
+
+ // SetFindMyMouseAppearanceBehavior(ref foundCustom, ref settings);
+ var excludedApps = foundCustom.Find("Excluded apps");
+ if (excludedApps != null)
+ {
+ excludedApps.Click();
+ excludedApps.Click();
+ }
+ else
+ {
+ Assert.Fail("Activation method group not found.");
+ }
+ }
+ else
+ {
+ Assert.Fail("Find My Mouse group not found.");
+ }
+
+ // [Test Case]Enable FindMyMouse. Then, without moving your mouse: Press Left Ctrl twice and verify the overlay appears.
+ // VerifySpotlightSettings(ref settings);
+
+ // [Test Case] Disable FindMyMouse. Verify the overlay no longer appears when you press Left Ctrl twice
+ foundCustom.Find("Enable Find My Mouse").Toggle(false);
+ Task.Delay(2000).Wait();
+ Session.SendKey(Key.LCtrl, 0, 0);
+ Task.Delay(100).Wait();
+ Session.SendKey(Key.LCtrl, 0, 0);
+
+ VerifySpotlightDisappears(ref settings);
+ }
+
+ private void VerifySpotlightDisappears(ref FindMyMouseSettings settings)
+ {
+ Task.Delay(2000).Wait();
+
+ var location = Session.GetMousePosition();
+ int radius = int.Parse(settings.Radius, CultureInfo.InvariantCulture);
+ var colorSpotlight = Session.GetPixelColorString(location.Item1, location.Item2);
+ Assert.AreNotEqual("#" + settings.SpotlightColor, colorSpotlight);
+
+ var colorBackground = Session.GetPixelColorString(location.Item1 + radius + 50, location.Item2 + radius + 50);
+ Assert.AreNotEqual("#" + settings.BackgroundColor, colorBackground);
+
+ var colorBackground2 = Session.GetPixelColorString(location.Item1 + radius + 100, location.Item2 + radius + 100);
+ Assert.AreNotEqual("#" + settings.BackgroundColor, colorBackground2);
+ }
+
+ private void VerifySpotlightAppears(ref FindMyMouseSettings settings)
+ {
+ Task.Delay(1000).Wait();
+
+ var location = Session.GetMousePosition();
+ int radius = int.Parse(settings.Radius, CultureInfo.InvariantCulture);
+ var colorSpotlight = Session.GetPixelColorString(location.Item1, location.Item2);
+ Assert.AreEqual("#" + settings.SpotlightColor, colorSpotlight);
+
+ var colorSpotlight2 = Session.GetPixelColorString(location.Item1 + radius - 1, location.Item2);
+
+ // Session.MoveMouseTo(location.Item1 + radius - 10, location.Item2);
+ Assert.AreEqual("#" + settings.SpotlightColor, colorSpotlight2);
+ Task.Delay(100).Wait();
+
+ var colorBackground = Session.GetPixelColorString(location.Item1 + radius + 50, location.Item2 + radius + 50);
+ Assert.AreEqual("#" + settings.BackgroundColor, colorBackground);
+
+ var colorBackground2 = Session.GetPixelColorString(location.Item1 + radius + 100, location.Item2 + radius + 100);
+ Assert.AreEqual("#" + settings.BackgroundColor, colorBackground2);
+ }
+
+ private void ActivateSpotlight(ref FindMyMouseSettings settings)
+ {
+ var xy = Session.GetMousePosition();
+ Session.MoveMouseTo(xy.Item1 - 200, xy.Item2 - 100);
+ Task.Delay(1000).Wait();
+
+ Session.PerformMouseAction(MouseActionType.LeftClick);
+ Task.Delay(5000).Wait();
+ if (settings.SelectedActivationMethod == FindMyMouseSettings.ActivationMethod.PressLeftControlTwice)
+ {
+ Session.SendKey(Key.LCtrl, 0, 0);
+ Task.Delay(100).Wait();
+ Session.SendKey(Key.LCtrl, 0, 0);
+ }
+ else if (settings.SelectedActivationMethod == FindMyMouseSettings.ActivationMethod.PressRightControlTwice)
+ {
+ Session.SendKey(Key.RCtrl, 0, 0);
+ Task.Delay(100).Wait();
+ Session.SendKey(Key.RCtrl, 0, 0);
+ }
+ else if (settings.SelectedActivationMethod == FindMyMouseSettings.ActivationMethod.ShakeMouse)
+ {
+ // Simulate shake mouse;
+ }
+ else if (settings.SelectedActivationMethod == FindMyMouseSettings.ActivationMethod.CustomShortcut)
+ {
+ // Simulate custom shortcut
+ }
+ }
+
+ private void VerifySpotlightSettings(ref FindMyMouseSettings settings, bool equal = true)
+ {
+ ActivateSpotlight(ref settings);
+
+ VerifySpotlightAppears(ref settings);
+ }
+
+ private void SetFindMyMouseActivationMethod(ref Custom? foundCustom, string method)
+ {
+ Assert.IsNotNull(foundCustom);
+ var groupActivation = foundCustom.Find("Activation method");
+ if (groupActivation != null)
+ {
+ groupActivation.Click();
+ string findMyMouseComboBoxKey = "Activation method";
+ var foundElements = foundCustom.FindAll(findMyMouseComboBoxKey);
+ if (foundElements.Count != 0)
+ {
+ var myMouseComboBox = foundCustom.Find(findMyMouseComboBoxKey);
+ Assert.IsNotNull(myMouseComboBox);
+ myMouseComboBox.Click();
+ var selectedItem = myMouseComboBox.Find(method);
+ Assert.IsNotNull(selectedItem);
+ selectedItem.Click();
+ }
+ else
+ {
+ Assert.IsTrue(false, "ComboBox is not found in the setting page.");
+ }
+ }
+ else
+ {
+ Assert.Fail("Activation method group not found.");
+ }
+ }
+
+ private void SetFindMyMouseAppearanceBehavior(ref Custom foundCustom, ref FindMyMouseSettings settings)
+ {
+ Assert.IsNotNull(foundCustom);
+ var groupAppearanceBehavior = foundCustom.Find("Appearance & behavior");
+ if (groupAppearanceBehavior != null)
+ {
+ // groupAppearanceBehavior.Click();
+ if (foundCustom.FindAll("Overlay opacity (%)").Count == 0)
+ {
+ groupAppearanceBehavior.Click();
+ }
+
+ // Set the BackGround color
+ var backgroundColor = foundCustom.Find("Background color");
+ Assert.IsNotNull(backgroundColor);
+
+ var button = backgroundColor.Find