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