From 18befd71491094946e9299b1e20467c2f7a702e2 Mon Sep 17 00:00:00 2001 From: Jerry Xu Date: Mon, 24 Feb 2025 15:24:33 +0800 Subject: [PATCH 01/43] Improve UITest Automation --- src/common/UITestAutomation/Element/Button.cs | 9 +-- src/common/UITestAutomation/Element/By.cs | 8 ++- .../UITestAutomation/Element/Element.cs | 69 +++++++++++-------- .../UITestAutomation/Element/TextBox.cs | 40 +++++++++++ .../UITestAutomation/Element/ToggleSwitch.cs | 41 +++++++++++ src/common/UITestAutomation/Element/Window.cs | 9 +-- .../FindElementHelper.cs => FindHelper.cs} | 18 +++-- src/common/UITestAutomation/Session.cs | 18 ++--- .../UITestAutomation/UITestAutomation.csproj | 35 +++++----- src/common/UITestAutomation/UITestBase.cs | 34 +++++++-- 10 files changed, 190 insertions(+), 91 deletions(-) create mode 100644 src/common/UITestAutomation/Element/TextBox.cs create mode 100644 src/common/UITestAutomation/Element/ToggleSwitch.cs rename src/common/UITestAutomation/{Element/FindElementHelper.cs => FindHelper.cs} (86%) diff --git a/src/common/UITestAutomation/Element/Button.cs b/src/common/UITestAutomation/Element/Button.cs index 44a7055fb2..610b0efb63 100644 --- a/src/common/UITestAutomation/Element/Button.cs +++ b/src/common/UITestAutomation/Element/Button.cs @@ -2,13 +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 Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenQA.Selenium; -using OpenQA.Selenium.Appium.Windows; -using OpenQA.Selenium.Interactions; -using OpenQA.Selenium.Remote; -using OpenQA.Selenium.Support.Events; - namespace Microsoft.PowerToys.UITest { /// @@ -17,4 +10,4 @@ namespace Microsoft.PowerToys.UITest public class Button : Element { } -} +} \ No newline at end of file diff --git a/src/common/UITestAutomation/Element/By.cs b/src/common/UITestAutomation/Element/By.cs index 11217ff61a..339cbe96be 100644 --- a/src/common/UITestAutomation/Element/By.cs +++ b/src/common/UITestAutomation/Element/By.cs @@ -18,6 +18,12 @@ namespace Microsoft.PowerToys.UITest this.by = by; } + public override string ToString() + { + // override ToString to return detailed debugging content provided by OpenQA.Selenium.By + return this.by.ToString(); + } + /// /// Creates a By object using the name attribute. /// @@ -66,4 +72,4 @@ namespace Microsoft.PowerToys.UITest /// An OpenQA.Selenium.By object. internal OpenQA.Selenium.By ToSeleniumBy() => by; } -} +} \ No newline at end of file diff --git a/src/common/UITestAutomation/Element/Element.cs b/src/common/UITestAutomation/Element/Element.cs index d4794507d6..e0b934b5ec 100644 --- a/src/common/UITestAutomation/Element/Element.cs +++ b/src/common/UITestAutomation/Element/Element.cs @@ -3,16 +3,11 @@ // See the LICENSE file in the project root for more information. using System.Collections.ObjectModel; -using System.Diagnostics; using System.Runtime.CompilerServices; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; using OpenQA.Selenium.Interactions; -using OpenQA.Selenium.Remote; -using OpenQA.Selenium.Support.Events; -using static Microsoft.PowerToys.UITest.UITestBase; [assembly: InternalsVisibleTo("Session")] @@ -24,6 +19,7 @@ namespace Microsoft.PowerToys.UITest public class Element { private WindowsElement? windowsElement; + private WindowsDriver? driver; internal void SetWindowsElement(WindowsElement windowsElement) => this.windowsElement = windowsElement; @@ -43,7 +39,20 @@ namespace Microsoft.PowerToys.UITest /// public string Text { - get { return GetAttribute("Value"); } + get { return this.windowsElement?.Text ?? string.Empty; } + } + + /// + /// Gets a value indicating whether the UI element is Enabled or not. + /// + public bool Enabled + { + get { return this.windowsElement?.Enabled ?? false; } + } + + public bool Selected + { + get { return this.windowsElement?.Selected ?? false; } } /// @@ -78,26 +87,17 @@ namespace Microsoft.PowerToys.UITest get { return GetAttribute("ControlType"); } } - /// - /// Checks if the UI element is enabled. - /// - /// True if the element is enabled; otherwise, false. - public bool IsEnabled() => GetAttribute("IsEnabled") == "True"; - - /// - /// Checks if the UI element is selected. - /// - /// True if the element is selected; otherwise, false. - public bool IsSelected() => GetAttribute("IsSelected") == "True"; - /// /// Click the UI element. /// - /// If true, performs a right-click; otherwise, performs a left-click. + /// If true, performs a right-click; otherwise, performs a left-click. Default value is false public void Click(bool rightClick = false) { - PerformAction(actions => + PerformAction((actions, windowElement) => { + actions.MoveToElement(windowElement); + actions.MoveByOffset(2, 2); + if (rightClick) { actions.ContextClick(); @@ -106,6 +106,8 @@ namespace Microsoft.PowerToys.UITest { actions.Click(); } + + actions.Build().Perform(); }); } @@ -133,7 +135,7 @@ namespace Microsoft.PowerToys.UITest where T : Element, new() { Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}"); - var foundElement = FindElementHelper.Find( + var foundElement = FindHelper.Find( () => { var element = this.windowsElement.FindElement(by.ToSeleniumBy()); @@ -157,7 +159,7 @@ namespace Microsoft.PowerToys.UITest where T : Element, new() { Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}"); - var foundElements = FindElementHelper.FindAll( + var foundElements = FindHelper.FindAll( () => { var elements = this.windowsElement.FindElements(by.ToSeleniumBy()); @@ -173,13 +175,24 @@ namespace Microsoft.PowerToys.UITest /// /// Simulates a manual operation on the element. /// - private void PerformAction(Action action) + /// The action to perform on the element. + /// The number of milliseconds to wait before the action. Default value is 100 ms + /// The number of milliseconds to wait after the action. Default value is 100 ms + protected void PerformAction(Action action, int msPreAction = 100, int msPostAction = 100) { - var element = this.windowsElement; + if (msPreAction > 0) + { + Task.Delay(msPreAction).Wait(); + } + + var windowElement = this.windowsElement!; Actions actions = new Actions(this.driver); - actions.MoveToElement(element); - action(actions); - actions.Build().Perform(); + action(actions, windowElement); + + if (msPostAction > 0) + { + Task.Delay(msPostAction).Wait(); + } } } -} +} \ No newline at end of file diff --git a/src/common/UITestAutomation/Element/TextBox.cs b/src/common/UITestAutomation/Element/TextBox.cs new file mode 100644 index 0000000000..041f3d510c --- /dev/null +++ b/src/common/UITestAutomation/Element/TextBox.cs @@ -0,0 +1,40 @@ +// 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 OpenQA.Selenium; + +namespace Microsoft.PowerToys.UITest +{ + /// + /// Represents a textbox in the UI test environment. + /// + public class TextBox : Element + { + /// + /// Sets the text of the textbox. + /// + /// The text to set. + /// A value indicating whether to clear the text before setting it. Default value is true + /// The current TextBox instance. + public TextBox SetText(string value, bool clearText = true) + { + if (clearText) + { + PerformAction((actions, windowElement) => + { + // select all text and delete it + windowElement.SendKeys(Keys.Control + "a"); + windowElement.SendKeys(Keys.Delete); + }); + } + + PerformAction((actions, windowElement) => + { + windowElement.SendKeys(value); + }); + + return this; + } + } +} \ No newline at end of file diff --git a/src/common/UITestAutomation/Element/ToggleSwitch.cs b/src/common/UITestAutomation/Element/ToggleSwitch.cs new file mode 100644 index 0000000000..e220a66ef9 --- /dev/null +++ b/src/common/UITestAutomation/Element/ToggleSwitch.cs @@ -0,0 +1,41 @@ +// 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 Newtonsoft.Json.Linq; + +namespace Microsoft.PowerToys.UITest +{ + /// + /// Represents a ToggleSwitch in the UI test environment. + /// + public class ToggleSwitch : Button + { + /// + /// Gets a value indicating whether indicates whether the ToggleSwitch is on. + /// + public bool IsOn + { + get + { + return this.Selected; + } + } + + /// + /// Sets the ToggleSwitch to the specified value. + /// + /// A value indicating whether the ToggleSwitch should be active. Default is true + /// The current ToggleSwitch instance. + public ToggleSwitch Toggle(bool value = true) + { + if (this.IsOn != value) + { + // Toggle the switch + this.Click(); + } + + return this; + } + } +} \ No newline at end of file diff --git a/src/common/UITestAutomation/Element/Window.cs b/src/common/UITestAutomation/Element/Window.cs index 558e0cb0b2..acfc4368c1 100644 --- a/src/common/UITestAutomation/Element/Window.cs +++ b/src/common/UITestAutomation/Element/Window.cs @@ -2,13 +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 Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenQA.Selenium; -using OpenQA.Selenium.Appium.Windows; -using OpenQA.Selenium.Interactions; -using OpenQA.Selenium.Remote; -using OpenQA.Selenium.Support.Events; - namespace Microsoft.PowerToys.UITest { /// @@ -89,4 +82,4 @@ namespace Microsoft.PowerToys.UITest } } } -} +} \ No newline at end of file diff --git a/src/common/UITestAutomation/Element/FindElementHelper.cs b/src/common/UITestAutomation/FindHelper.cs similarity index 86% rename from src/common/UITestAutomation/Element/FindElementHelper.cs rename to src/common/UITestAutomation/FindHelper.cs index 4ed42a9639..8add7df46b 100644 --- a/src/common/UITestAutomation/Element/FindElementHelper.cs +++ b/src/common/UITestAutomation/FindHelper.cs @@ -2,16 +2,9 @@ // 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.Collections.ObjectModel; -using System.Linq; using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenQA.Selenium; -using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; [assembly: InternalsVisibleTo("Element")] @@ -22,7 +15,7 @@ namespace Microsoft.PowerToys.UITest /// /// Helper class for finding elements. /// - internal static class FindElementHelper + internal static class FindHelper { public static T Find(Func findElementFunc, WindowsDriver? driver, int timeoutMS) where T : Element, new() @@ -51,10 +44,15 @@ namespace Microsoft.PowerToys.UITest Assert.IsNotNull(element, $"New Element {typeof(T).Name} error: element is null."); T newElement = new T(); - driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(timeoutMS); + if (timeoutMS > 0) + { + // Only set timeout if it is positive value + driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(timeoutMS); + } + newElement.SetSession(driver); newElement.SetWindowsElement(element); return newElement; } } -} +} \ No newline at end of file diff --git a/src/common/UITestAutomation/Session.cs b/src/common/UITestAutomation/Session.cs index 2e21cc392e..62e63d8867 100644 --- a/src/common/UITestAutomation/Session.cs +++ b/src/common/UITestAutomation/Session.cs @@ -2,17 +2,11 @@ // 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.ObjectModel; -using System.IO; -using System.Reflection; using System.Runtime.InteropServices; -using System.Xml.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; -using OpenQA.Selenium.Interactions; namespace Microsoft.PowerToys.UITest { @@ -45,7 +39,7 @@ namespace Microsoft.PowerToys.UITest where T : Element, new() { Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}"); - var foundElement = FindElementHelper.Find( + var foundElement = FindHelper.Find( () => { var element = this.WindowsDriver.FindElement(by.ToSeleniumBy()); @@ -65,21 +59,20 @@ namespace Microsoft.PowerToys.UITest /// The selector to find the elements. /// The timeout in milliseconds (default is 3000). /// A read-only collection of the found elements. - public ReadOnlyCollection? FindAll(By by, int timeoutMS = 3000) + public ReadOnlyCollection FindAll(By by, int timeoutMS = 3000) where T : Element, new() { Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}"); - var foundElements = FindElementHelper.FindAll( + var foundElements = FindHelper.FindAll( () => { var elements = this.WindowsDriver.FindElements(by.ToSeleniumBy()); - Assert.IsTrue(elements.Count > 0, $"Elements not found using selector: {by}"); return elements; }, this.WindowsDriver, timeoutMS); - return foundElements; + return foundElements ?? new ReadOnlyCollection(new List()); } /// @@ -110,6 +103,7 @@ namespace Microsoft.PowerToys.UITest 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); @@ -126,4 +120,4 @@ namespace Microsoft.PowerToys.UITest return this; } } -} +} \ No newline at end of file diff --git a/src/common/UITestAutomation/UITestAutomation.csproj b/src/common/UITestAutomation/UITestAutomation.csproj index 0f2e53681d..1589a6aaf3 100644 --- a/src/common/UITestAutomation/UITestAutomation.csproj +++ b/src/common/UITestAutomation/UITestAutomation.csproj @@ -1,21 +1,18 @@ - + + + - - Library - net9.0 - enable - enable - true - true - + + Library + enable + enable + true + true + - - - - - - - - - - + + + + + + \ No newline at end of file diff --git a/src/common/UITestAutomation/UITestBase.cs b/src/common/UITestAutomation/UITestBase.cs index ac69ad04b1..fe8fc00261 100644 --- a/src/common/UITestAutomation/UITestBase.cs +++ b/src/common/UITestAutomation/UITestBase.cs @@ -2,17 +2,13 @@ // 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.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Reflection; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; -using OpenQA.Selenium.Interactions; namespace Microsoft.PowerToys.UITest { @@ -37,6 +33,34 @@ namespace Microsoft.PowerToys.UITest this.testInit.Cleanup(); } + /// + /// Finds an element by selector. + /// Shortcut for this.Session.Find(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 3000). + /// The found element. + protected T Find(By by, int timeoutMS = 3000) + where T : Element, new() + { + return this.Session.Find(by, 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). + /// A read-only collection of the found elements. + protected ReadOnlyCollection FindAll(By by, int timeoutMS = 3000) + where T : Element, new() + { + return this.Session.FindAll(by, timeoutMS); + } + /// /// Nested class for test initialization. /// @@ -127,4 +151,4 @@ namespace Microsoft.PowerToys.UITest } } } -} +} \ No newline at end of file From 8c21f794af19c14dd85fe913a45fbfcb36dbf8ed Mon Sep 17 00:00:00 2001 From: Jerry Xu Date: Mon, 24 Feb 2025 15:24:33 +0800 Subject: [PATCH 02/43] Improve UITest Automation --- src/common/UITestAutomation/Element/Button.cs | 9 +-- src/common/UITestAutomation/Element/By.cs | 8 ++- .../UITestAutomation/Element/Element.cs | 69 +++++++++++-------- .../UITestAutomation/Element/TextBox.cs | 40 +++++++++++ .../UITestAutomation/Element/ToggleSwitch.cs | 41 +++++++++++ src/common/UITestAutomation/Element/Window.cs | 9 +-- .../FindElementHelper.cs => FindHelper.cs} | 18 +++-- src/common/UITestAutomation/Session.cs | 18 ++--- .../UITestAutomation/UITestAutomation.csproj | 6 +- src/common/UITestAutomation/UITestBase.cs | 34 +++++++-- 10 files changed, 176 insertions(+), 76 deletions(-) create mode 100644 src/common/UITestAutomation/Element/TextBox.cs create mode 100644 src/common/UITestAutomation/Element/ToggleSwitch.cs rename src/common/UITestAutomation/{Element/FindElementHelper.cs => FindHelper.cs} (86%) diff --git a/src/common/UITestAutomation/Element/Button.cs b/src/common/UITestAutomation/Element/Button.cs index 44a7055fb2..610b0efb63 100644 --- a/src/common/UITestAutomation/Element/Button.cs +++ b/src/common/UITestAutomation/Element/Button.cs @@ -2,13 +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 Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenQA.Selenium; -using OpenQA.Selenium.Appium.Windows; -using OpenQA.Selenium.Interactions; -using OpenQA.Selenium.Remote; -using OpenQA.Selenium.Support.Events; - namespace Microsoft.PowerToys.UITest { /// @@ -17,4 +10,4 @@ namespace Microsoft.PowerToys.UITest public class Button : Element { } -} +} \ No newline at end of file diff --git a/src/common/UITestAutomation/Element/By.cs b/src/common/UITestAutomation/Element/By.cs index 11217ff61a..339cbe96be 100644 --- a/src/common/UITestAutomation/Element/By.cs +++ b/src/common/UITestAutomation/Element/By.cs @@ -18,6 +18,12 @@ namespace Microsoft.PowerToys.UITest this.by = by; } + public override string ToString() + { + // override ToString to return detailed debugging content provided by OpenQA.Selenium.By + return this.by.ToString(); + } + /// /// Creates a By object using the name attribute. /// @@ -66,4 +72,4 @@ namespace Microsoft.PowerToys.UITest /// An OpenQA.Selenium.By object. internal OpenQA.Selenium.By ToSeleniumBy() => by; } -} +} \ No newline at end of file diff --git a/src/common/UITestAutomation/Element/Element.cs b/src/common/UITestAutomation/Element/Element.cs index d4794507d6..e0b934b5ec 100644 --- a/src/common/UITestAutomation/Element/Element.cs +++ b/src/common/UITestAutomation/Element/Element.cs @@ -3,16 +3,11 @@ // See the LICENSE file in the project root for more information. using System.Collections.ObjectModel; -using System.Diagnostics; using System.Runtime.CompilerServices; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; using OpenQA.Selenium.Interactions; -using OpenQA.Selenium.Remote; -using OpenQA.Selenium.Support.Events; -using static Microsoft.PowerToys.UITest.UITestBase; [assembly: InternalsVisibleTo("Session")] @@ -24,6 +19,7 @@ namespace Microsoft.PowerToys.UITest public class Element { private WindowsElement? windowsElement; + private WindowsDriver? driver; internal void SetWindowsElement(WindowsElement windowsElement) => this.windowsElement = windowsElement; @@ -43,7 +39,20 @@ namespace Microsoft.PowerToys.UITest /// public string Text { - get { return GetAttribute("Value"); } + get { return this.windowsElement?.Text ?? string.Empty; } + } + + /// + /// Gets a value indicating whether the UI element is Enabled or not. + /// + public bool Enabled + { + get { return this.windowsElement?.Enabled ?? false; } + } + + public bool Selected + { + get { return this.windowsElement?.Selected ?? false; } } /// @@ -78,26 +87,17 @@ namespace Microsoft.PowerToys.UITest get { return GetAttribute("ControlType"); } } - /// - /// Checks if the UI element is enabled. - /// - /// True if the element is enabled; otherwise, false. - public bool IsEnabled() => GetAttribute("IsEnabled") == "True"; - - /// - /// Checks if the UI element is selected. - /// - /// True if the element is selected; otherwise, false. - public bool IsSelected() => GetAttribute("IsSelected") == "True"; - /// /// Click the UI element. /// - /// If true, performs a right-click; otherwise, performs a left-click. + /// If true, performs a right-click; otherwise, performs a left-click. Default value is false public void Click(bool rightClick = false) { - PerformAction(actions => + PerformAction((actions, windowElement) => { + actions.MoveToElement(windowElement); + actions.MoveByOffset(2, 2); + if (rightClick) { actions.ContextClick(); @@ -106,6 +106,8 @@ namespace Microsoft.PowerToys.UITest { actions.Click(); } + + actions.Build().Perform(); }); } @@ -133,7 +135,7 @@ namespace Microsoft.PowerToys.UITest where T : Element, new() { Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}"); - var foundElement = FindElementHelper.Find( + var foundElement = FindHelper.Find( () => { var element = this.windowsElement.FindElement(by.ToSeleniumBy()); @@ -157,7 +159,7 @@ namespace Microsoft.PowerToys.UITest where T : Element, new() { Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}"); - var foundElements = FindElementHelper.FindAll( + var foundElements = FindHelper.FindAll( () => { var elements = this.windowsElement.FindElements(by.ToSeleniumBy()); @@ -173,13 +175,24 @@ namespace Microsoft.PowerToys.UITest /// /// Simulates a manual operation on the element. /// - private void PerformAction(Action action) + /// The action to perform on the element. + /// The number of milliseconds to wait before the action. Default value is 100 ms + /// The number of milliseconds to wait after the action. Default value is 100 ms + protected void PerformAction(Action action, int msPreAction = 100, int msPostAction = 100) { - var element = this.windowsElement; + if (msPreAction > 0) + { + Task.Delay(msPreAction).Wait(); + } + + var windowElement = this.windowsElement!; Actions actions = new Actions(this.driver); - actions.MoveToElement(element); - action(actions); - actions.Build().Perform(); + action(actions, windowElement); + + if (msPostAction > 0) + { + Task.Delay(msPostAction).Wait(); + } } } -} +} \ No newline at end of file diff --git a/src/common/UITestAutomation/Element/TextBox.cs b/src/common/UITestAutomation/Element/TextBox.cs new file mode 100644 index 0000000000..041f3d510c --- /dev/null +++ b/src/common/UITestAutomation/Element/TextBox.cs @@ -0,0 +1,40 @@ +// 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 OpenQA.Selenium; + +namespace Microsoft.PowerToys.UITest +{ + /// + /// Represents a textbox in the UI test environment. + /// + public class TextBox : Element + { + /// + /// Sets the text of the textbox. + /// + /// The text to set. + /// A value indicating whether to clear the text before setting it. Default value is true + /// The current TextBox instance. + public TextBox SetText(string value, bool clearText = true) + { + if (clearText) + { + PerformAction((actions, windowElement) => + { + // select all text and delete it + windowElement.SendKeys(Keys.Control + "a"); + windowElement.SendKeys(Keys.Delete); + }); + } + + PerformAction((actions, windowElement) => + { + windowElement.SendKeys(value); + }); + + return this; + } + } +} \ No newline at end of file diff --git a/src/common/UITestAutomation/Element/ToggleSwitch.cs b/src/common/UITestAutomation/Element/ToggleSwitch.cs new file mode 100644 index 0000000000..e220a66ef9 --- /dev/null +++ b/src/common/UITestAutomation/Element/ToggleSwitch.cs @@ -0,0 +1,41 @@ +// 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 Newtonsoft.Json.Linq; + +namespace Microsoft.PowerToys.UITest +{ + /// + /// Represents a ToggleSwitch in the UI test environment. + /// + public class ToggleSwitch : Button + { + /// + /// Gets a value indicating whether indicates whether the ToggleSwitch is on. + /// + public bool IsOn + { + get + { + return this.Selected; + } + } + + /// + /// Sets the ToggleSwitch to the specified value. + /// + /// A value indicating whether the ToggleSwitch should be active. Default is true + /// The current ToggleSwitch instance. + public ToggleSwitch Toggle(bool value = true) + { + if (this.IsOn != value) + { + // Toggle the switch + this.Click(); + } + + return this; + } + } +} \ No newline at end of file diff --git a/src/common/UITestAutomation/Element/Window.cs b/src/common/UITestAutomation/Element/Window.cs index 558e0cb0b2..acfc4368c1 100644 --- a/src/common/UITestAutomation/Element/Window.cs +++ b/src/common/UITestAutomation/Element/Window.cs @@ -2,13 +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 Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenQA.Selenium; -using OpenQA.Selenium.Appium.Windows; -using OpenQA.Selenium.Interactions; -using OpenQA.Selenium.Remote; -using OpenQA.Selenium.Support.Events; - namespace Microsoft.PowerToys.UITest { /// @@ -89,4 +82,4 @@ namespace Microsoft.PowerToys.UITest } } } -} +} \ No newline at end of file diff --git a/src/common/UITestAutomation/Element/FindElementHelper.cs b/src/common/UITestAutomation/FindHelper.cs similarity index 86% rename from src/common/UITestAutomation/Element/FindElementHelper.cs rename to src/common/UITestAutomation/FindHelper.cs index 4ed42a9639..8add7df46b 100644 --- a/src/common/UITestAutomation/Element/FindElementHelper.cs +++ b/src/common/UITestAutomation/FindHelper.cs @@ -2,16 +2,9 @@ // 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.Collections.ObjectModel; -using System.Linq; using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenQA.Selenium; -using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; [assembly: InternalsVisibleTo("Element")] @@ -22,7 +15,7 @@ namespace Microsoft.PowerToys.UITest /// /// Helper class for finding elements. /// - internal static class FindElementHelper + internal static class FindHelper { public static T Find(Func findElementFunc, WindowsDriver? driver, int timeoutMS) where T : Element, new() @@ -51,10 +44,15 @@ namespace Microsoft.PowerToys.UITest Assert.IsNotNull(element, $"New Element {typeof(T).Name} error: element is null."); T newElement = new T(); - driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(timeoutMS); + if (timeoutMS > 0) + { + // Only set timeout if it is positive value + driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(timeoutMS); + } + newElement.SetSession(driver); newElement.SetWindowsElement(element); return newElement; } } -} +} \ No newline at end of file diff --git a/src/common/UITestAutomation/Session.cs b/src/common/UITestAutomation/Session.cs index 2e21cc392e..62e63d8867 100644 --- a/src/common/UITestAutomation/Session.cs +++ b/src/common/UITestAutomation/Session.cs @@ -2,17 +2,11 @@ // 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.ObjectModel; -using System.IO; -using System.Reflection; using System.Runtime.InteropServices; -using System.Xml.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; -using OpenQA.Selenium.Interactions; namespace Microsoft.PowerToys.UITest { @@ -45,7 +39,7 @@ namespace Microsoft.PowerToys.UITest where T : Element, new() { Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}"); - var foundElement = FindElementHelper.Find( + var foundElement = FindHelper.Find( () => { var element = this.WindowsDriver.FindElement(by.ToSeleniumBy()); @@ -65,21 +59,20 @@ namespace Microsoft.PowerToys.UITest /// The selector to find the elements. /// The timeout in milliseconds (default is 3000). /// A read-only collection of the found elements. - public ReadOnlyCollection? FindAll(By by, int timeoutMS = 3000) + public ReadOnlyCollection FindAll(By by, int timeoutMS = 3000) where T : Element, new() { Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}"); - var foundElements = FindElementHelper.FindAll( + var foundElements = FindHelper.FindAll( () => { var elements = this.WindowsDriver.FindElements(by.ToSeleniumBy()); - Assert.IsTrue(elements.Count > 0, $"Elements not found using selector: {by}"); return elements; }, this.WindowsDriver, timeoutMS); - return foundElements; + return foundElements ?? new ReadOnlyCollection(new List()); } /// @@ -110,6 +103,7 @@ namespace Microsoft.PowerToys.UITest 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); @@ -126,4 +120,4 @@ namespace Microsoft.PowerToys.UITest return this; } } -} +} \ No newline at end of file diff --git a/src/common/UITestAutomation/UITestAutomation.csproj b/src/common/UITestAutomation/UITestAutomation.csproj index 0f2e53681d..0da17d85b8 100644 --- a/src/common/UITestAutomation/UITestAutomation.csproj +++ b/src/common/UITestAutomation/UITestAutomation.csproj @@ -1,8 +1,9 @@  + + Library - net9.0 enable enable true @@ -15,7 +16,4 @@ - - - diff --git a/src/common/UITestAutomation/UITestBase.cs b/src/common/UITestAutomation/UITestBase.cs index ac69ad04b1..fe8fc00261 100644 --- a/src/common/UITestAutomation/UITestBase.cs +++ b/src/common/UITestAutomation/UITestBase.cs @@ -2,17 +2,13 @@ // 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.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Reflection; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; -using OpenQA.Selenium.Interactions; namespace Microsoft.PowerToys.UITest { @@ -37,6 +33,34 @@ namespace Microsoft.PowerToys.UITest this.testInit.Cleanup(); } + /// + /// Finds an element by selector. + /// Shortcut for this.Session.Find(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 3000). + /// The found element. + protected T Find(By by, int timeoutMS = 3000) + where T : Element, new() + { + return this.Session.Find(by, 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). + /// A read-only collection of the found elements. + protected ReadOnlyCollection FindAll(By by, int timeoutMS = 3000) + where T : Element, new() + { + return this.Session.FindAll(by, timeoutMS); + } + /// /// Nested class for test initialization. /// @@ -127,4 +151,4 @@ namespace Microsoft.PowerToys.UITest } } } -} +} \ No newline at end of file From 8687b310db0d8dd40dddef17c034d7098f23e051 Mon Sep 17 00:00:00 2001 From: Jerry Xu Date: Mon, 24 Feb 2025 15:43:56 +0800 Subject: [PATCH 03/43] Exclude all UI-Test projects instead of just fancyZone UITest --- .pipelines/verifyDepsJsonLibraryVersions.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pipelines/verifyDepsJsonLibraryVersions.ps1 b/.pipelines/verifyDepsJsonLibraryVersions.ps1 index 84397661d5..b91a8a566c 100644 --- a/.pipelines/verifyDepsJsonLibraryVersions.ps1 +++ b/.pipelines/verifyDepsJsonLibraryVersions.ps1 @@ -15,8 +15,8 @@ Param( $referencedFileVersionsPerDll = @{} $totalFailures = 0 -Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude UITests-FancyZones*,MouseJump.Common.UnitTests*,*.FuzzTests* | ForEach-Object { - # Temporarily exclude FancyZones UI tests because of Appium.WebDriver dependencies +Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude *UITests*,MouseJump.Common.UnitTests*,*.FuzzTests* | ForEach-Object { + # Temporarily exclude All UI-Test, Fuzzer-Test projects because of Appium.WebDriver dependencies $depsJsonFullFileName = $_.FullName $depsJsonFileName = $_.Name $depsJson = Get-Content $depsJsonFullFileName | ConvertFrom-Json From 3874a3b8934212a9c0dc2ee3f2a221af91f21ec4 Mon Sep 17 00:00:00 2001 From: Jerry Xu Date: Mon, 24 Feb 2025 15:43:56 +0800 Subject: [PATCH 04/43] Exclude all UI-Test projects instead of just fancyZone UITest --- .pipelines/verifyDepsJsonLibraryVersions.ps1 | 4 ++-- src/common/UITestAutomation/Element/ToggleSwitch.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pipelines/verifyDepsJsonLibraryVersions.ps1 b/.pipelines/verifyDepsJsonLibraryVersions.ps1 index 84397661d5..b91a8a566c 100644 --- a/.pipelines/verifyDepsJsonLibraryVersions.ps1 +++ b/.pipelines/verifyDepsJsonLibraryVersions.ps1 @@ -15,8 +15,8 @@ Param( $referencedFileVersionsPerDll = @{} $totalFailures = 0 -Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude UITests-FancyZones*,MouseJump.Common.UnitTests*,*.FuzzTests* | ForEach-Object { - # Temporarily exclude FancyZones UI tests because of Appium.WebDriver dependencies +Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude *UITests*,MouseJump.Common.UnitTests*,*.FuzzTests* | ForEach-Object { + # Temporarily exclude All UI-Test, Fuzzer-Test projects because of Appium.WebDriver dependencies $depsJsonFullFileName = $_.FullName $depsJsonFileName = $_.Name $depsJson = Get-Content $depsJsonFullFileName | ConvertFrom-Json diff --git a/src/common/UITestAutomation/Element/ToggleSwitch.cs b/src/common/UITestAutomation/Element/ToggleSwitch.cs index e220a66ef9..a05ce9b6e2 100644 --- a/src/common/UITestAutomation/Element/ToggleSwitch.cs +++ b/src/common/UITestAutomation/Element/ToggleSwitch.cs @@ -12,7 +12,7 @@ namespace Microsoft.PowerToys.UITest public class ToggleSwitch : Button { /// - /// Gets a value indicating whether indicates whether the ToggleSwitch is on. + /// Gets a value indicating whether the ToggleSwitch is on. /// public bool IsOn { From d243b58715da7a489bdf2b0b58eda278e2a01392 Mon Sep 17 00:00:00 2001 From: Jerry Xu Date: Mon, 24 Feb 2025 15:51:53 +0800 Subject: [PATCH 05/43] Fix code-style --- src/common/UITestAutomation/Element/Button.cs | 2 +- src/common/UITestAutomation/Element/By.cs | 2 +- src/common/UITestAutomation/Element/Element.cs | 2 +- src/common/UITestAutomation/Element/TextBox.cs | 2 +- src/common/UITestAutomation/Element/ToggleSwitch.cs | 2 +- src/common/UITestAutomation/Element/Window.cs | 2 +- src/common/UITestAutomation/FindHelper.cs | 2 +- src/common/UITestAutomation/Session.cs | 2 +- src/common/UITestAutomation/UITestBase.cs | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/common/UITestAutomation/Element/Button.cs b/src/common/UITestAutomation/Element/Button.cs index 610b0efb63..b5915031be 100644 --- a/src/common/UITestAutomation/Element/Button.cs +++ b/src/common/UITestAutomation/Element/Button.cs @@ -10,4 +10,4 @@ namespace Microsoft.PowerToys.UITest public class Button : Element { } -} \ No newline at end of file +} diff --git a/src/common/UITestAutomation/Element/By.cs b/src/common/UITestAutomation/Element/By.cs index 339cbe96be..5ccaf1eb06 100644 --- a/src/common/UITestAutomation/Element/By.cs +++ b/src/common/UITestAutomation/Element/By.cs @@ -72,4 +72,4 @@ namespace Microsoft.PowerToys.UITest /// An OpenQA.Selenium.By object. internal OpenQA.Selenium.By ToSeleniumBy() => by; } -} \ No newline at end of file +} diff --git a/src/common/UITestAutomation/Element/Element.cs b/src/common/UITestAutomation/Element/Element.cs index 699ed22ea9..f2f3d4ee29 100644 --- a/src/common/UITestAutomation/Element/Element.cs +++ b/src/common/UITestAutomation/Element/Element.cs @@ -214,4 +214,4 @@ namespace Microsoft.PowerToys.UITest } } } -} \ No newline at end of file +} diff --git a/src/common/UITestAutomation/Element/TextBox.cs b/src/common/UITestAutomation/Element/TextBox.cs index 041f3d510c..e427f7c9d3 100644 --- a/src/common/UITestAutomation/Element/TextBox.cs +++ b/src/common/UITestAutomation/Element/TextBox.cs @@ -37,4 +37,4 @@ namespace Microsoft.PowerToys.UITest return this; } } -} \ No newline at end of file +} diff --git a/src/common/UITestAutomation/Element/ToggleSwitch.cs b/src/common/UITestAutomation/Element/ToggleSwitch.cs index a05ce9b6e2..b2a7f1b607 100644 --- a/src/common/UITestAutomation/Element/ToggleSwitch.cs +++ b/src/common/UITestAutomation/Element/ToggleSwitch.cs @@ -38,4 +38,4 @@ namespace Microsoft.PowerToys.UITest return this; } } -} \ No newline at end of file +} diff --git a/src/common/UITestAutomation/Element/Window.cs b/src/common/UITestAutomation/Element/Window.cs index acfc4368c1..eceb1fcf47 100644 --- a/src/common/UITestAutomation/Element/Window.cs +++ b/src/common/UITestAutomation/Element/Window.cs @@ -82,4 +82,4 @@ namespace Microsoft.PowerToys.UITest } } } -} \ No newline at end of file +} diff --git a/src/common/UITestAutomation/FindHelper.cs b/src/common/UITestAutomation/FindHelper.cs index 8add7df46b..241aa6b339 100644 --- a/src/common/UITestAutomation/FindHelper.cs +++ b/src/common/UITestAutomation/FindHelper.cs @@ -55,4 +55,4 @@ namespace Microsoft.PowerToys.UITest return newElement; } } -} \ No newline at end of file +} diff --git a/src/common/UITestAutomation/Session.cs b/src/common/UITestAutomation/Session.cs index 62e63d8867..7071f6d2e2 100644 --- a/src/common/UITestAutomation/Session.cs +++ b/src/common/UITestAutomation/Session.cs @@ -120,4 +120,4 @@ namespace Microsoft.PowerToys.UITest return this; } } -} \ No newline at end of file +} diff --git a/src/common/UITestAutomation/UITestBase.cs b/src/common/UITestAutomation/UITestBase.cs index fe8fc00261..4ce432f23a 100644 --- a/src/common/UITestAutomation/UITestBase.cs +++ b/src/common/UITestAutomation/UITestBase.cs @@ -151,4 +151,4 @@ namespace Microsoft.PowerToys.UITest } } } -} \ No newline at end of file +} From fd206ecdee37c60c55b0ea1d6229b6b53158a4e3 Mon Sep 17 00:00:00 2001 From: Zhaopeng Wang Date: Tue, 11 Mar 2025 15:37:32 +0800 Subject: [PATCH 06/43] add Automation clean up code --- src/common/UITestAutomation/Session.cs | 37 ++++++++++++++++++-- src/common/UITestAutomation/SessionHelper.cs | 37 ++++++++++++++++++++ src/common/UITestAutomation/UITestBase.cs | 15 +++++--- 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/common/UITestAutomation/Session.cs b/src/common/UITestAutomation/Session.cs index ef0a6fff3f..3536528b3d 100644 --- a/src/common/UITestAutomation/Session.cs +++ b/src/common/UITestAutomation/Session.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.ObjectModel; +using System.Diagnostics; using System.Runtime.InteropServices; using System.Xml.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -20,6 +21,8 @@ namespace Microsoft.PowerToys.UITest private WindowsDriver WindowsDriver { get; set; } + private List windowList = new List(); + [DllImport("user32.dll")] private static extern bool SetForegroundWindow(nint hWnd); @@ -29,6 +32,29 @@ namespace Microsoft.PowerToys.UITest this.WindowsDriver = windowsDriver; } + /// + /// Cleans up the Session Exe. + /// + public void Cleanup() + { + for (var i = 0; i < this.windowList.Count; i++) + { + var windowHandle = this.windowList[i]; + if (windowHandle != 0) + { + var process = Process.GetProcessById((int)windowHandle); + + if (process != null) + { + process.Kill(); + process.WaitForExit(); + } + } + } + + windowList.Clear(); + } + /// /// Finds an element by selector. /// @@ -44,7 +70,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.AreNotEqual(0, collection.Count, $"Element not found using selector: {by}"); return collection[0]; } @@ -166,7 +192,12 @@ namespace Microsoft.PowerToys.UITest { if (this.Root != null) { - var window = this.Root.FindElementByName(windowName); + var window = this.Root.FindElementByName("Administrator: " + windowName); + if (window == null) + { + window = this.Root.FindElementByName(windowName); + } + Assert.IsNotNull(window, $"Failed to attach. Window '{windowName}' not found"); var windowHandle = new nint(int.Parse(window.GetAttribute("NativeWindowHandle"))); @@ -179,6 +210,8 @@ namespace Microsoft.PowerToys.UITest this.WindowsDriver = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), appCapabilities); Assert.IsNotNull(this.WindowsDriver, "Attach WindowsDriver is null"); + this.windowList.Add(windowHandle); + // Set implicit timeout to make element search retry every 500 ms this.WindowsDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3); } diff --git a/src/common/UITestAutomation/SessionHelper.cs b/src/common/UITestAutomation/SessionHelper.cs index 7bb1f6e7a6..09a1380a12 100644 --- a/src/common/UITestAutomation/SessionHelper.cs +++ b/src/common/UITestAutomation/SessionHelper.cs @@ -35,6 +35,7 @@ namespace Microsoft.PowerToys.UITest Verb = "runas", }; + this.ExitExe(winAppDriverProcessInfo.FileName); this.appDriver = Process.Start(winAppDriverProcessInfo); var desktopCapabilities = new AppiumOptions(); @@ -53,6 +54,7 @@ namespace Microsoft.PowerToys.UITest public SessionHelper Init() { string? path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + this.ExitExe(path + this.sessionPath); this.StartExe(path + this.sessionPath); Assert.IsNotNull(this.Driver, $"Failed to initialize the test environment. Driver is null."); @@ -68,9 +70,11 @@ namespace Microsoft.PowerToys.UITest /// public void Cleanup() { + this.ExitScopeExe(); try { appDriver?.Kill(); + appDriver?.WaitForExit(); } catch (Exception ex) { @@ -79,6 +83,39 @@ namespace Microsoft.PowerToys.UITest } } + /// + /// Exit a exe. + /// + /// The path to the application executable. + public void ExitExe(string path) + { + // Exit Exe + string exeName = Path.GetFileNameWithoutExtension(path); + + // PowerToys.FancyZonesEditor + Process[] processes = Process.GetProcessesByName(exeName); + foreach (Process process in processes) + { + try + { + process.Kill(); + process.WaitForExit(); // Optional: Wait for the process to exit + } + catch (Exception ex) + { + Assert.Fail($"Failed to terminate process {process.ProcessName} (ID: {process.Id}): {ex.Message}"); + } + } + } + + /// + /// Exit now exe. + /// + public void ExitScopeExe() + { + this.ExitExe(sessionPath); + } + /// /// Starts a new exe and takes control of it. /// diff --git a/src/common/UITestAutomation/UITestBase.cs b/src/common/UITestAutomation/UITestBase.cs index 1d6502ac54..f4c0794b86 100644 --- a/src/common/UITestAutomation/UITestBase.cs +++ b/src/common/UITestAutomation/UITestBase.cs @@ -31,11 +31,6 @@ namespace Microsoft.PowerToys.UITest this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver()); } - ~UITestBase() - { - this.sessionHelper.Cleanup(); - } - /// /// Initializes the test. /// @@ -53,6 +48,16 @@ namespace Microsoft.PowerToys.UITest } } + /// + /// Initializes the test. + /// + [TestCleanup] + public void TestCleanup() + { + this.Session.Cleanup(); + this.sessionHelper.Cleanup(); + } + /// /// Finds an element by selector. /// Shortcut for this.Session.Find(by, timeoutMS) From 63c4089441814e4368e3ea26768d2d8969b8140c Mon Sep 17 00:00:00 2001 From: "Xiaofeng Wang (from Dev Box)" Date: Tue, 11 Mar 2025 15:53:20 +0800 Subject: [PATCH 07/43] Run UI tests in CI pipeline --- .pipelines/v2/templates/job-test-project.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.pipelines/v2/templates/job-test-project.yml b/.pipelines/v2/templates/job-test-project.yml index d5252ba23b..0bbaeae7e4 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\** From 22efeb3f6321524b136b1321a9441cc1ae6aafc0 Mon Sep 17 00:00:00 2001 From: Jerry Xu Date: Wed, 12 Mar 2025 13:37:23 +0800 Subject: [PATCH 08/43] Improve UIAutomation to support: 1. SetWindowSize 2. Auto-close 3. Better window search logic --- .../UITestAutomation/ModuleConfigData.cs | 41 +++ src/common/UITestAutomation/Session.cs | 279 +++++++++++++++++- src/common/UITestAutomation/SessionHelper.cs | 5 +- src/common/UITestAutomation/UITestBase.cs | 132 ++++++++- .../Hosts/Hosts.UITests/HostModuleTests.cs | 64 ++-- .../Hosts/Hosts.UITests/HostsSettingTests.cs | 4 +- 6 files changed, 476 insertions(+), 49 deletions(-) diff --git a/src/common/UITestAutomation/ModuleConfigData.cs b/src/common/UITestAutomation/ModuleConfigData.cs index d8dd1cac5a..1b97546a37 100644 --- a/src/common/UITestAutomation/ModuleConfigData.cs +++ b/src/common/UITestAutomation/ModuleConfigData.cs @@ -31,6 +31,47 @@ namespace Microsoft.PowerToys.UITest Hosts, } + /// + /// 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/Session.cs b/src/common/UITestAutomation/Session.cs index ef0a6fff3f..ac1deb54b7 100644 --- a/src/common/UITestAutomation/Session.cs +++ b/src/common/UITestAutomation/Session.cs @@ -4,8 +4,10 @@ using System.Collections.ObjectModel; using System.Runtime.InteropServices; +using System.Text; using System.Xml.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; @@ -20,17 +22,33 @@ namespace Microsoft.PowerToys.UITest 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) + /// + /// 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. + /// 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. @@ -85,7 +103,103 @@ namespace Microsoft.PowerToys.UITest } /// - /// 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 3000). + /// True if only has one element, otherwise false. + public bool HasOne(By by, int timeoutMS = 3000) + 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 3000). + /// True if only has one element, otherwise false. + public bool HasOne(By by, int timeoutMS = 3000) + { + 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 3000). + /// True if only has one element, otherwise false. + public bool HasOne(string name, int timeoutMS = 3000) + 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 3000). + /// True if only has one element, otherwise false. + public bool HasOne(string name, int timeoutMS = 3000) + { + 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 3000). + /// True if has one or more element, otherwise false. + public bool Has(By by, int timeoutMS = 3000) + 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 3000). + /// True if has one or more element, otherwise false. + public bool Has(By by, int timeoutMS = 3000) + { + 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 3000). + /// True if has one or more element, otherwise false. + public bool Has(string name, int timeoutMS = 3000) + 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 3000). + /// True if has one or more element, otherwise false. + public bool Has(string name, int timeoutMS = 3000) + { + 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. @@ -145,15 +259,80 @@ namespace Microsoft.PowerToys.UITest return this.FindAll(By.Name(name), timeoutMS); } + /// + /// Sets the main window size. + /// + /// WindowSize enum + public void SetMainWindowSize(WindowSize size) + { + if (size == WindowSize.UnSpecified) + { + return; + } + + 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); + } + /// /// 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); } /// @@ -161,26 +340,42 @@ 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; + 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]); - var windowHandle = new nint(int.Parse(window.GetAttribute("NativeWindowHandle"))); - SetForegroundWindow(windowHandle); - var hexWindowHandle = windowHandle.ToString("x"); + if (matchingWindows.Count == 0) + { + 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 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); + + if (size != WindowSize.UnSpecified) + { + this.SetMainWindowSize(size); + } } else { @@ -189,5 +384,61 @@ 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); + + 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; + } + } } } diff --git a/src/common/UITestAutomation/SessionHelper.cs b/src/common/UITestAutomation/SessionHelper.cs index 7bb1f6e7a6..4a73a44d4d 100644 --- a/src/common/UITestAutomation/SessionHelper.cs +++ b/src/common/UITestAutomation/SessionHelper.cs @@ -52,7 +52,7 @@ namespace Microsoft.PowerToys.UITest [UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "")] public SessionHelper Init() { - string? path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string? path = @"C:\Users\nxu\AppData\Local\PowerToys\1\2\3"; // Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); this.StartExe(path + this.sessionPath); Assert.IsNotNull(this.Driver, $"Failed to initialize the test environment. Driver is null."); @@ -70,7 +70,8 @@ namespace Microsoft.PowerToys.UITest { try { - appDriver?.Kill(); + Driver!.CloseApp(); + appDriver?.Kill(true); } catch (Exception ex) { diff --git a/src/common/UITestAutomation/UITestBase.cs b/src/common/UITestAutomation/UITestBase.cs index 1d6502ac54..4053a7a944 100644 --- a/src/common/UITestAutomation/UITestBase.cs +++ b/src/common/UITestAutomation/UITestBase.cs @@ -18,22 +18,16 @@ namespace Microsoft.PowerToys.UITest [TestClass] public class UITestBase { - public Session Session { get; set; } - - private readonly SessionHelper sessionHelper; + public required Session Session { get; set; } private readonly PowerToysModule scope; + private readonly WindowSize size; + private SessionHelper? sessionHelper; - public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings) + public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified) { this.scope = scope; - this.sessionHelper = new SessionHelper(scope).Init(); - this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver()); - } - - ~UITestBase() - { - this.sessionHelper.Cleanup(); + this.size = size; } /// @@ -42,6 +36,9 @@ namespace Microsoft.PowerToys.UITest [TestInitialize] public void TestInit() { + 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 @@ -53,6 +50,19 @@ namespace Microsoft.PowerToys.UITest } } + /// + /// Cleanups the test + /// + [TestCleanup] + public void TestCleanup() + { + if (this.sessionHelper != null) + { + this.sessionHelper.Cleanup(); + this.sessionHelper = null; + } + } + /// /// Finds an element by selector. /// Shortcut for this.Session.Find(by, timeoutMS) @@ -68,7 +78,7 @@ namespace Microsoft.PowerToys.UITest } /// - /// 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. @@ -92,7 +102,7 @@ namespace Microsoft.PowerToys.UITest } /// - /// 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). @@ -102,6 +112,102 @@ namespace Microsoft.PowerToys.UITest 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 3000). + /// True if only has one element, otherwise false. + public bool HasOne(By by, int timeoutMS = 3000) + 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 3000). + /// True if only has one element, otherwise false. + public bool HasOne(By by, int timeoutMS = 3000) + { + 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 3000). + /// True if only has one element, otherwise false. + public bool HasOne(string name, int timeoutMS = 3000) + 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 3000). + /// True if only has one element, otherwise false. + public bool HasOne(string name, int timeoutMS = 3000) + { + 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 3000). + /// True if has one or more element, otherwise false. + public bool Has(By by, int timeoutMS = 3000) + 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 3000). + /// True if has one or more element, otherwise false. + public bool Has(By by, int timeoutMS = 3000) + { + 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 3000). + /// True if has one or more element, otherwise false. + public bool Has(string name, int timeoutMS = 3000) + 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 3000). + /// True if has one or more element, otherwise false. + public bool Has(string name, int timeoutMS = 3000) + { + return this.Session.Has(name, timeoutMS); + } + /// /// Finds all elements by selector. /// Shortcut for this.Session.FindAll(by, timeoutMS) diff --git a/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs b/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs index c50bbe988e..1e57dcfb35 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,14 @@ 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"); // Click 'Add an entry' from empty-view for adding Host override rule this.Find("Add an entry").Click(); @@ -46,8 +46,8 @@ 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 - [TestMethod] + [TestMethod("Hosts.Basic.ErrorMessgeShowupIfNotRunAsAdmin")] public void TestErrorMessageWithNonAdminPermission() { - this.CloseWarningDialog(); - this.RemoveAllEntries(); + if (this.Session.IsElevated == false) + { + this.CloseWarningDialog(); + this.RemoveAllEntries(); - // Add new URL override and a warning tip should be shown - this.AddEntry("192.168.0.1", "localhost", true); + // Add new URL override and a warning tip should be shown + this.AddEntry("192.168.0.1", "localhost", true); - Assert.IsTrue( - this.FindAll("The hosts file cannot be saved because the program isn't running as administrator.").Count == 1, - "Should display host-file saving error if not run as administrator"); + Assert.IsTrue( + this.FindAll("The hosts file cannot be saved because the program isn't running as administrator.").Count == 1, + "Should display host-file saving error if not run as administrator"); + } + } + + /// + /// Test No Error-message in the Hosts-File-Editor + /// + /// + /// Validating error message should be shown if not run as admin. + /// + /// + /// + [TestMethod("Hosts.Basic.NoErrorMessgeShowupIfRunAsAdmin")] + public void TestNoErrorMessageWithNonAdminPermission() + { + if (this.Session.IsElevated == true) + { + this.CloseWarningDialog(); + this.RemoveAllEntries(); + + // Add new URL override and a warning tip should be shown + this.AddEntry("192.168.0.1", "localhost", true); + + Assert.IsFalse( + this.FindAll("The hosts file cannot be saved because the program isn't running as administrator.").Count == 1, + "Should display host-file saving error if not run as administrator"); + } } /// @@ -144,7 +172,7 @@ namespace Hosts.UITests /// /// /// - [TestMethod] + [TestMethod("Hosts.Basic.FiltersControlShouldWork")] public void TestFilterControl() { this.CloseWarningDialog(); diff --git a/src/modules/Hosts/Hosts.UITests/HostsSettingTests.cs b/src/modules/Hosts/Hosts.UITests/HostsSettingTests.cs index c8ab562602..edc0480d46 100644 --- a/src/modules/Hosts/Hosts.UITests/HostsSettingTests.cs +++ b/src/modules/Hosts/Hosts.UITests/HostsSettingTests.cs @@ -29,7 +29,7 @@ namespace Hosts.UITests /// /// /// - [TestMethod] + [TestMethod("Hosts.Settings.ShowWarningDialogIfRunAsAdmin")] public void TestWarningDialog() { this.LaunchFromSetting(showWarning: true); @@ -54,7 +54,7 @@ namespace Hosts.UITests // wait for 500 ms to make sure Hosts File Editor is launched Task.Delay(500).Wait(); - this.Session.Attach(PowerToysModule.Hosts); + this.Session.Attach(PowerToysModule.Hosts, WindowSize.Small_Vertical); // Should show warning dialog Assert.IsTrue(this.FindAll("Warning").Count > 0, "Should show warning dialog"); From 9c08c957e58a488c564f9d9c7c6af260df411bc4 Mon Sep 17 00:00:00 2001 From: Zhaopeng Wang Date: Tue, 11 Mar 2025 15:37:32 +0800 Subject: [PATCH 09/43] add Automation clean up code Rebase from origin/dev/nxu/ImproveUIAutomation --- src/common/UITestAutomation/Session.cs | 45 ++++++++++++++------ src/common/UITestAutomation/SessionHelper.cs | 42 ++++++++++++++++-- src/common/UITestAutomation/UITestBase.cs | 9 ++-- 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/src/common/UITestAutomation/Session.cs b/src/common/UITestAutomation/Session.cs index 50d94977ba..4458a7db03 100644 --- a/src/common/UITestAutomation/Session.cs +++ b/src/common/UITestAutomation/Session.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.ObjectModel; +using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; using System.Xml.Linq; @@ -22,6 +23,8 @@ namespace Microsoft.PowerToys.UITest private WindowsDriver WindowsDriver { get; set; } private const string AdministratorPrefix = "Administrator: "; + + private List windowList = new List(); /// /// Gets Main Window Handler @@ -46,6 +49,29 @@ namespace Microsoft.PowerToys.UITest this.Attach(scope, size); } + /// + /// Cleans up the Session Exe. + /// + public void Cleanup() + { + for (var i = 0; i < this.windowList.Count; i++) + { + var windowHandle = this.windowList[i]; + if (windowHandle != 0) + { + var process = Process.GetProcessById((int)windowHandle); + + if (process != null) + { + process.Kill(); + process.WaitForExit(); + } + } + } + + windowList.Clear(); + } + /// /// Finds an Element or its derived class by selector. /// @@ -61,7 +87,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.AreNotEqual(0, collection.Count, $"Element not found using selector: {by}"); return collection[0]; } @@ -347,19 +373,8 @@ namespace Microsoft.PowerToys.UITest if (this.Root != null) { - // search window handler by window title (admin and non-admin titles) - var matchingWindows = ApiHelper.FindDesktopWindowHandler([windowName, AdministratorPrefix + windowName]); - - if (matchingWindows.Count == 0) - { - 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 window = this.Root.FindElementByName(windowName); + Assert.IsNotNull(window, $"Failed to attach. Window '{windowName}' not found"); var hexWindowHandle = this.MainWindowHandler.ToInt64().ToString("x"); var appCapabilities = new AppiumOptions(); @@ -368,6 +383,8 @@ namespace Microsoft.PowerToys.UITest appCapabilities.AddAdditionalCapability("deviceName", "WindowsPC"); this.WindowsDriver = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), appCapabilities); + this.windowList.Add(this.MainWindowHandler); + // Set implicit timeout to make element search retry every 500 ms this.WindowsDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3); diff --git a/src/common/UITestAutomation/SessionHelper.cs b/src/common/UITestAutomation/SessionHelper.cs index 4a73a44d4d..09a1380a12 100644 --- a/src/common/UITestAutomation/SessionHelper.cs +++ b/src/common/UITestAutomation/SessionHelper.cs @@ -35,6 +35,7 @@ namespace Microsoft.PowerToys.UITest Verb = "runas", }; + this.ExitExe(winAppDriverProcessInfo.FileName); this.appDriver = Process.Start(winAppDriverProcessInfo); var desktopCapabilities = new AppiumOptions(); @@ -52,7 +53,8 @@ namespace Microsoft.PowerToys.UITest [UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "")] public SessionHelper Init() { - string? path = @"C:\Users\nxu\AppData\Local\PowerToys\1\2\3"; // Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string? path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + this.ExitExe(path + this.sessionPath); this.StartExe(path + this.sessionPath); Assert.IsNotNull(this.Driver, $"Failed to initialize the test environment. Driver is null."); @@ -68,10 +70,11 @@ namespace Microsoft.PowerToys.UITest /// public void Cleanup() { + this.ExitScopeExe(); try { - Driver!.CloseApp(); - appDriver?.Kill(true); + appDriver?.Kill(); + appDriver?.WaitForExit(); } catch (Exception ex) { @@ -80,6 +83,39 @@ namespace Microsoft.PowerToys.UITest } } + /// + /// Exit a exe. + /// + /// The path to the application executable. + public void ExitExe(string path) + { + // Exit Exe + string exeName = Path.GetFileNameWithoutExtension(path); + + // PowerToys.FancyZonesEditor + Process[] processes = Process.GetProcessesByName(exeName); + foreach (Process process in processes) + { + try + { + process.Kill(); + process.WaitForExit(); // Optional: Wait for the process to exit + } + catch (Exception ex) + { + Assert.Fail($"Failed to terminate process {process.ProcessName} (ID: {process.Id}): {ex.Message}"); + } + } + } + + /// + /// Exit now exe. + /// + public void ExitScopeExe() + { + this.ExitExe(sessionPath); + } + /// /// Starts a new exe and takes control of it. /// diff --git a/src/common/UITestAutomation/UITestBase.cs b/src/common/UITestAutomation/UITestBase.cs index 4053a7a944..13c7ea3f48 100644 --- a/src/common/UITestAutomation/UITestBase.cs +++ b/src/common/UITestAutomation/UITestBase.cs @@ -51,16 +51,13 @@ namespace Microsoft.PowerToys.UITest } /// - /// Cleanups the test + /// Cleanups the test. /// [TestCleanup] public void TestCleanup() { - if (this.sessionHelper != null) - { - this.sessionHelper.Cleanup(); - this.sessionHelper = null; - } + this.Session.Cleanup(); + this.sessionHelper.Cleanup(); } /// From a10578f7d3346bd7676eaad6bf074deb135f061d Mon Sep 17 00:00:00 2001 From: "Xiaofeng Wang (from Dev Box)" Date: Tue, 11 Mar 2025 15:53:20 +0800 Subject: [PATCH 10/43] Run UI tests in CI pipeline --- .pipelines/v2/templates/job-test-project.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.pipelines/v2/templates/job-test-project.yml b/.pipelines/v2/templates/job-test-project.yml index d5252ba23b..0bbaeae7e4 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\** From 127e079efe16863df0d8b8559674c1b9020af4c2 Mon Sep 17 00:00:00 2001 From: Jerry Xu Date: Wed, 12 Mar 2025 14:13:35 +0800 Subject: [PATCH 11/43] fix build issue --- .../UITestAutomation/Element/Element.cs | 44 +++++++++++++++++-- src/common/UITestAutomation/Session.cs | 2 - src/common/UITestAutomation/SessionHelper.cs | 16 ++----- src/common/UITestAutomation/UITestBase.cs | 2 +- 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/common/UITestAutomation/Element/Element.cs b/src/common/UITestAutomation/Element/Element.cs index 8314660176..59c799e401 100644 --- a/src/common/UITestAutomation/Element/Element.cs +++ b/src/common/UITestAutomation/Element/Element.cs @@ -98,8 +98,8 @@ namespace Microsoft.PowerToys.UITest /// /// Click the UI element. /// - /// If true, performs a right-click; otherwise, performs a left-click. - public void Click(bool rightClick = false) + /// If true, performs a right-click; otherwise, performs a left-click. Default value is false + public virtual void Click(bool rightClick = false) { PerformAction((actions, windowElement) => { @@ -116,6 +116,8 @@ namespace Microsoft.PowerToys.UITest { actions.Click(); } + + actions.Build().Perform(); }); } @@ -168,6 +170,32 @@ namespace Microsoft.PowerToys.UITest return collection[0]; } + /// + /// Finds an element by the selector. + /// Shortcut for this.Find(By.Name(name), timeoutMS) + /// + /// The class type of the element to find. + /// The name for finding the element. + /// The timeout in milliseconds. + /// The found element. + public T Find(string name, int timeoutMS = 3000) + where T : Element, new() + { + return this.Find(By.Name(name), timeoutMS); + } + + /// + /// Finds an element by the selector. + /// Shortcut for this.Find(by, timeoutMS) + /// + /// The selector to use for finding the element. + /// The timeout in milliseconds. + /// The found element. + public Element Find(By by, int timeoutMS = 3000) + { + return this.Find(by, timeoutMS); + } + /// /// Finds an element by the selector. /// Shortcut for this.Find(By.Name(name), timeoutMS) @@ -244,9 +272,17 @@ namespace Microsoft.PowerToys.UITest /// /// Simulates a manual operation on the element. /// - private void PerformAction(Action action) + /// 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) { - var element = this.windowsElement; + if (msPreAction > 0) + { + Task.Delay(msPreAction).Wait(); + } + + var windowElement = this.windowsElement!; Actions actions = new Actions(this.driver); action(actions, windowElement); diff --git a/src/common/UITestAutomation/Session.cs b/src/common/UITestAutomation/Session.cs index 91bcf2da9f..d5c03bdf14 100644 --- a/src/common/UITestAutomation/Session.cs +++ b/src/common/UITestAutomation/Session.cs @@ -390,8 +390,6 @@ namespace Microsoft.PowerToys.UITest this.windowList.Add(this.MainWindowHandler); - this.windowList.Add(windowHandle); - // Set implicit timeout to make element search retry every 500 ms this.WindowsDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3); diff --git a/src/common/UITestAutomation/SessionHelper.cs b/src/common/UITestAutomation/SessionHelper.cs index 09a1380a12..5772792958 100644 --- a/src/common/UITestAutomation/SessionHelper.cs +++ b/src/common/UITestAutomation/SessionHelper.cs @@ -70,7 +70,7 @@ namespace Microsoft.PowerToys.UITest /// public void Cleanup() { - this.ExitScopeExe(); + this.ExitExe(this.sessionPath); try { appDriver?.Kill(); @@ -86,11 +86,11 @@ namespace Microsoft.PowerToys.UITest /// /// 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); @@ -108,14 +108,6 @@ namespace Microsoft.PowerToys.UITest } } - /// - /// Exit now exe. - /// - public void ExitScopeExe() - { - this.ExitExe(sessionPath); - } - /// /// Starts a new exe and takes control of it. /// diff --git a/src/common/UITestAutomation/UITestBase.cs b/src/common/UITestAutomation/UITestBase.cs index 13c7ea3f48..88aeb07e86 100644 --- a/src/common/UITestAutomation/UITestBase.cs +++ b/src/common/UITestAutomation/UITestBase.cs @@ -57,7 +57,7 @@ namespace Microsoft.PowerToys.UITest public void TestCleanup() { this.Session.Cleanup(); - this.sessionHelper.Cleanup(); + this.sessionHelper!.Cleanup(); } /// From 1867ac8f023243958de76737888e3300f766e4d2 Mon Sep 17 00:00:00 2001 From: Jerry Xu Date: Wed, 12 Mar 2025 15:51:40 +0800 Subject: [PATCH 12/43] Support VisualAssert - Image based validation --- .../UITestAutomation/Element/Element.cs | 10 ++ src/common/UITestAutomation/Session.cs | 42 +++-- src/common/UITestAutomation/SessionHelper.cs | 7 +- src/common/UITestAutomation/VisualAssert.cs | 146 ++++++++++++++++++ .../HostModuleTests_TestAddingEntry.png | Bin 0 -> 22420 bytes ...ostModuleTests_TestEmptyView_EmptyView.png | Bin 0 -> 24079 bytes ...ModuleTests_TestEmptyView_NonEmptyView.png | Bin 0 -> 14097 bytes .../Hosts/Hosts.UITests/HostModuleTests.cs | 8 +- .../Hosts/Hosts.UITests/Hosts.UITests.csproj | 5 + 9 files changed, 200 insertions(+), 18 deletions(-) create mode 100644 src/common/UITestAutomation/VisualAssert.cs create mode 100644 src/modules/Hosts/Hosts.UITests/Baseline/HostModuleTests_TestAddingEntry.png create mode 100644 src/modules/Hosts/Hosts.UITests/Baseline/HostModuleTests_TestEmptyView_EmptyView.png create mode 100644 src/modules/Hosts/Hosts.UITests/Baseline/HostModuleTests_TestEmptyView_NonEmptyView.png diff --git a/src/common/UITestAutomation/Element/Element.cs b/src/common/UITestAutomation/Element/Element.cs index 59c799e401..bc41ba5903 100644 --- a/src/common/UITestAutomation/Element/Element.cs +++ b/src/common/UITestAutomation/Element/Element.cs @@ -291,5 +291,15 @@ namespace Microsoft.PowerToys.UITest Task.Delay(msPostAction).Wait(); } } + + /// + /// Save UI Element to a PNG file. + /// + /// the full path + internal void SaveToPngFile(string path) + { + Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method SaveToFile with parameter: path = {path}"); + this.windowsElement.GetScreenshot().SaveAsFile(path); + } } } diff --git a/src/common/UITestAutomation/Session.cs b/src/common/UITestAutomation/Session.cs index d5c03bdf14..718f53f3e2 100644 --- a/src/common/UITestAutomation/Session.cs +++ b/src/common/UITestAutomation/Session.cs @@ -3,10 +3,8 @@ // See the LICENSE file in the project root for more information. using System.Collections.ObjectModel; -using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; -using System.Xml.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; @@ -24,7 +22,7 @@ namespace Microsoft.PowerToys.UITest private const string AdministratorPrefix = "Administrator: "; - private List windowList = new List(); + private List windowHandlers = new List(); /// /// Gets Main Window Handler @@ -54,22 +52,30 @@ namespace Microsoft.PowerToys.UITest /// public void Cleanup() { - for (var i = 0; i < this.windowList.Count; i++) + /* + foreach (var windowHandle in this.windowHandlers) { - var windowHandle = this.windowList[i]; - if (windowHandle != 0) + if (windowHandle == IntPtr.Zero) + { + continue; + } + + try { var process = Process.GetProcessById((int)windowHandle); - - if (process != null) + if (process != null && !process.HasExited) { process.Kill(); process.WaitForExit(); } } + catch + { + } } + */ - windowList.Clear(); + windowHandlers.Clear(); } /// @@ -370,25 +376,31 @@ namespace Microsoft.PowerToys.UITest 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("Administrator: " + windowName); - if (window == null) + // 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) { - window = this.Root.FindElementByName(windowName); + Assert.Fail($"Failed to attach. Window '{windowName}' not found"); } - Assert.IsNotNull(window, $"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 appCapabilities = new AppiumOptions(); + var appCapabilities = new AppiumOptions(); appCapabilities.AddAdditionalCapability("appTopLevelWindow", hexWindowHandle); appCapabilities.AddAdditionalCapability("deviceName", "WindowsPC"); this.WindowsDriver = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), appCapabilities); - this.windowList.Add(this.MainWindowHandler); + this.windowHandlers.Add(this.MainWindowHandler); // Set implicit timeout to make element search retry every 500 ms this.WindowsDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3); diff --git a/src/common/UITestAutomation/SessionHelper.cs b/src/common/UITestAutomation/SessionHelper.cs index 5772792958..8d50e743b5 100644 --- a/src/common/UITestAutomation/SessionHelper.cs +++ b/src/common/UITestAutomation/SessionHelper.cs @@ -98,8 +98,11 @@ namespace Microsoft.PowerToys.UITest { try { - process.Kill(); - process.WaitForExit(); // Optional: Wait for the process to exit + if (!process.HasExited) + { + process.Kill(); + process.WaitForExit(); // Optional: Wait for the process to exit + } } catch (Exception ex) { diff --git a/src/common/UITestAutomation/VisualAssert.cs b/src/common/UITestAutomation/VisualAssert.cs new file mode 100644 index 0000000000..6b1be71268 --- /dev/null +++ b/src/common/UITestAutomation/VisualAssert.cs @@ -0,0 +1,146 @@ +// 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 + /// + /// 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(Element element, 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 callerAssembly = callerMethod!.DeclaringType!.Assembly; + var baselineImageResourceName = callerAssembly.GetManifestResourceNames().Where(name => name.Contains(scenarioSubname)).FirstOrDefault(); + + var tempTestImagePath = GetTempFilePath(scenarioSubname, "test", ".png"); + element.SaveToPngFile(tempTestImagePath); + + 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; + + using (var baselineImage = new Bitmap(callerAssembly.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); + } + } + } + + if (!isSame) + { + 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 (!VisualAssert.PixIsSame(baselineImage.GetPixel(x, y), testImage.GetPixel(x, y))) + { + return false; + } + } + } + + return true; + } + + /// + /// Compare two pixels with a fuzz factor + /// + /// base color + /// test color + /// fuzz factor, default is 10 + /// true if same, otherwise is false + private 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; + } + } +} 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 0000000000000000000000000000000000000000..738ed5f380254fbda6b2726257c74d183828d39e GIT binary patch literal 22420 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 QP&xw^WLwf56YseH0g)LR{r~^~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..88c1b75de54b2f4d10dbe06889a25dc0dcfdccdd GIT binary patch literal 14097 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 vQhN!5O-)Iw@BpQdw("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(); @@ -47,7 +49,9 @@ namespace Hosts.UITests // Should have one row now and not more empty view Assert.IsTrue(this.Has /// If true, performs a right-click; otherwise, performs a left-click. Default value is false - /// Mouse click hold time. Default value is 300 ms - public virtual void Click(bool rightClick = false, int clickHoldMS = 300) + public virtual void Click(bool rightClick = false) { PerformAction((actions, windowElement) => { @@ -111,14 +110,14 @@ namespace Microsoft.PowerToys.UITest if (rightClick) { - actions.ContextClick().Build().Perform(); + actions.ContextClick(); } else { - actions.ClickAndHold().Build().Perform(); - Task.Delay(clickHoldMS).Wait(); - actions.Release().Build().Perform(); + actions.Click(); } + + actions.Build().Perform(); }); } @@ -297,10 +296,15 @@ namespace Microsoft.PowerToys.UITest /// Save UI Element to a PNG file. /// /// the full path - internal void SaveToPngFile(string 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/NavigationViewItem.cs b/src/common/UITestAutomation/Element/NavigationViewItem.cs index 59b02ec4cf..0a71d9a321 100644 --- a/src/common/UITestAutomation/Element/NavigationViewItem.cs +++ b/src/common/UITestAutomation/Element/NavigationViewItem.cs @@ -24,26 +24,22 @@ namespace Microsoft.PowerToys.UITest /// Click the ListItem element. /// /// If true, performs a right-click; otherwise, performs a left-click. Default value is false - /// Mouse click hold time. Default value is 300 ms - public override void Click(bool rightClick = false, int clickHoldMS = 300) + public override void Click(bool rightClick = false) { PerformAction((actions, windowElement) => { - actions.MoveToElement(windowElement); - - // Move 2by2 offset to make click more stable instead of click on the border of the element - actions.MoveByOffset(10, 10); + actions.MoveToElement(windowElement, 10, 10); if (rightClick) { - actions.ContextClick().Build().Perform(); + actions.ContextClick(); } else { - actions.ClickAndHold().Build().Perform(); - Task.Delay(clickHoldMS).Wait(); - actions.Release().Build().Perform(); + actions.Click(); } + + actions.Build().Perform(); }); } diff --git a/src/common/UITestAutomation/VisualAssert.cs b/src/common/UITestAutomation/VisualAssert.cs index 6b1be71268..3abb77e912 100644 --- a/src/common/UITestAutomation/VisualAssert.cs +++ b/src/common/UITestAutomation/VisualAssert.cs @@ -51,7 +51,9 @@ namespace Microsoft.PowerToys.UITest var baselineImageResourceName = callerAssembly.GetManifestResourceNames().Where(name => name.Contains(scenarioSubname)).FirstOrDefault(); var tempTestImagePath = GetTempFilePath(scenarioSubname, "test", ".png"); - element.SaveToPngFile(tempTestImagePath); + + // Save the image with the user preference color erased + element.SaveToPngFile(tempTestImagePath, true); if (string.IsNullOrEmpty(baselineImageResourceName) || !Path.GetFileNameWithoutExtension(baselineImageResourceName).EndsWith(scenarioSubname)) @@ -121,7 +123,7 @@ namespace Microsoft.PowerToys.UITest { for (int y = excludeBorderHeight; y < baselineImage.Height - excludeBorderHeight; y++) { - if (!VisualAssert.PixIsSame(baselineImage.GetPixel(x, y), testImage.GetPixel(x, y))) + if (!VisualHelper.PixIsSame(baselineImage.GetPixel(x, y), testImage.GetPixel(x, y))) { return false; } @@ -130,17 +132,5 @@ namespace Microsoft.PowerToys.UITest return true; } - - /// - /// Compare two pixels with a fuzz factor - /// - /// base color - /// test color - /// fuzz factor, default is 10 - /// true if same, otherwise is false - private 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; - } } } diff --git a/src/common/UITestAutomation/VisualHelper.cs b/src/common/UITestAutomation/VisualHelper.cs new file mode 100644 index 0000000000..64d8258114 --- /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 5. + private static void MakeColorTransparent(string imagePath, string outputPath, Color targetColor, int fuzz = 5) + { + 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 5. + public static void EraseUserPreferenceColor(string imagePath, int fuzz = 5) + { + 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 5. + /// True if the hues are the same, otherwise false. + public static bool HueIsSame(Color c1, Color c2, int fuzz = 5) + { + 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 index 738ed5f380254fbda6b2726257c74d183828d39e..5a2ccaa07c008f20c3414dcbd7bf47a5be0a5a1e 100644 GIT binary patch literal 6649 zcmeHM>0480(?5z^3s^S_0s(zoi>xYP34+jCvP}Lhcr~ z739W-^l)<)bK9j+LgBH~$7ZuGEd0ezDr>|i@x~C8;4*LKwV4l>?pe7d+S5?%dS=%A z^jOO+LFv0VOc+z);RG;v#pK*FQ7bodEXIr zLEKE#{cXGkc}A631{O7CdZJ_yxMsb{ zAS}}LD&wlF%9ma>WiAJR^jBd<8l8M|MoAuS3jla+YRtW5(CM?IF(v@eVgUet7DhmP zS7wd}SKE_IwztWg8E)m#xs3DnpKlxr6P*!F^P+RI(yu`3TB{#jfUEnFTwtfd+V3px)y3)%x%H|7nMtkV)Eysx&f!T{dts$K~Oiak({HSAFH60U+|iJ^=0ma z%o9!;I$#jokIh+Da);V?l`p9LxgN>U>}vfc{+>dw+lDb}Jgf@jt8^Kjb`++?ePHgl5BT2bckmp==gh(Vtvrq9k-G5Ui0tn`je7gQ^?sF>85H{A!n+^IG11Zd;tFS zyZW-dc{)fYvGx%4rjHJtSqgDTe!Y2~)?M>O)F%*WSX83K906P(Y8q*PCi7wi8d zQMZf0VdK)<&U(JED#BdZG}AREW~KPs7%8|5RXEJ?M1g1ffg+-HL-i$dz2697D`T|P z10Dy(c$tooI$bO>2le+N(DQvN{fi(>onKwEt$DJB{Y6A;%&l+z-yMAk;yC+Gosekz zwT;IF2-9gx0sRQcR{bxHlu43jBv_C3am?LKV;1kQWB{C^@0hXpd1zW)B2`GCB>jB! zgy5Hy`;8)%-i?|OOA~&>XsZ)`yqYo^g7!n&3qFsT{he8@84K13ea2p!6uB-uy=&=} zB+R039)zejp7B$sx>{o$1b;#5MrcOTQTAwdc}JC=5r&Jx@N>5OMw*H1j+UU`)cvS( z`#>}5F9k=k2$xSLUXUNf(QTKf+H!g+dnAr{I<%C;LsoUdlzlrv2Sjr|+=d_8-jaT< zm%9Cdoat=S$x(j77zS} zhywXI#^Uc?s*WSwVwmoAfRjVBb}XL<9y%=3WmofsTB*l}8+ANpk!!CcV^K;Wqnj2c zs<6>K3no5LfGr*g$4g(4`MAkQDT|PN1OJ5#3gF52o>z>wD z3(zE8j2_3qHfJ;=;SIA`P~i!l(tp{}8cXCVOz_3VQuH@9ljVbzKP!u6_~nNkV$So+ zowro&PTZ{YYVQbSkvIJI<<_|M*fI*AINRf&i#ZgoQ)L8^DbY!m1MMwLzlPra%NK)h zGSKk~OV3o9Ov)(o42LEf7m#Le7|qP`P_j%#%Di}lpDunf_(+TPaa>jh5j~R?@R4PB zSa64HYyVY)HwR`2Jwdl|+@3__*;m*rqEDay+yZhFsso`5r23Q6TwNb zYwCH826M3R<4L<%H%NUyfkx)BUfytVtuswRDlwa^EHi=f+Bd(+V`nrG9*(=C%=nD1 zdI+DDaF6w%8Uw3`NITU1+S^SdxJO2f!fTSElc!-Cr%IMA>cd{lTX&3wMso~82;cVO zB7eAWF5$V4n`& zjv|O1huWMS%^tvbFp4Yf5h2n-lk;0%|J`OC&T&5?0W6lFZCn()Lb{bIViS!)qwr~Y z1~*kcApf5qp4k_GxW~Mj_`CO8WntiVw{XYk|7j!*2UrBtlLnZtHLoSq<22d0JNp~<_`kPFVjoo$~>h7xH~1roi5)>ZlqctIp%#^ zRf|23cY=)z1|%z?F3IKU(+zPpxHLkeD)3FqSH{s+2lj!E#l}!v_wcz)bAHMY*X?f>Cs8xgklAg^kN&4_Cs=r2w%R%X;BnA0LG{D(l3$ zrX@!DN!46|Ta8eJpJaauHS1Ye3(=m!j6110J_OkF4_YJJ64BjED6^{aGHD285`>lq zjuM3ETe$9rq-Qj<>#`=6+&#sGWfu;@4tf@V%eW^Ja#Yhu2LBqe!HY;$i7eVX4B4~K zf*;ptbmGJ%DMm27_X)ypHfXatU5V`3DvqKEF=myRr(Nr%jWJ#XQ_i6iFxf>K4xixp zgitY_qE^o(TBp>QilYswb|_MR_mzAv2kz>Y-_)LVxZu!H5SWa7tBQs#(I4RB&teym z?}}ItCb8C-rqNyQro%RfhGv#Xa=JTIYn5<-JeQG!_)v78l|@fZ1QeW=<6%m@5l$;BPoZYA+ry~%7z3>Ev zKdyo<>#uYh3>0$iXxLYTR_*(t@Cu1N_8zKE=)3`Nr`qPsmN5KM$cy8=`BM#7WTRo4 zq;63U*Cl#xqzbk7BjC9it)J+4sT$?zUX1lcE3H`|Dulb0te;zScTGdCWy@9&iKwfk z1J}7jnY_d2;0mqz9;MJ^&*gJW-l348i@$F(z@OqX2#>|t2C_WRT2z$4uGhW18MFfU zf|i8L3*}S&B}zw|>?JA{Oh$c)3Ro(Zpyu<_toIeH0gfBkjvGK7u*J|KY*|5y1{uJQ z1`~i4=x6|Dt1PR2S-lLaL9&_|R@32XnOLojtAJn?D6OKyRn+`nX%z@O?c)4QZnR-) zDEWJrVLIMk`9syMs-He{L^7{xJLN+Sg(mBO>(3_ps8P8qQ}L_w)9-mniDD)$*1%pG zCwwk!G0etAG!$a-gJIt9zhUkF8=>Dr%IvC-YyjT)hRBDM4ORO;ZWhMITJs5%pTPrw zfweav@1sB-GyV?Mb%oVgJ}MK@bGGvziz0ecYhiW8c0oN56B6ziiP^IXO7Rd{Qn8ejkXn s0)Vs-S1beTn!reKmG`qAEpL2e*Y9KQzBAOYt^rP+2>OcVck#ym04C}>rvLx| literal 22420 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 QP&xw^WLwf56YseH0g)LR{r~^~ diff --git a/src/modules/Hosts/Hosts.UITests/Baseline/HostModuleTests_TestEmptyView_NonEmptyView.png b/src/modules/Hosts/Hosts.UITests/Baseline/HostModuleTests_TestEmptyView_NonEmptyView.png index 88c1b75de54b2f4d10dbe06889a25dc0dcfdccdd..ed85e4c31cea26b5ff9b3ace93599eeb37c383a5 100644 GIT binary patch literal 7069 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 WPsU|Opy{Chz|lh?2k8f*ulyVI1{r+- literal 14097 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 vQhN!5O-)Iw@BpQdw("The hosts file cannot be saved because the program isn't running as administrator.").Count == 1, + this.FindAll("The hosts file cannot be saved because the program isn't running as administrator.").Count > 0, "Should display host-file saving error if not run as administrator"); } } @@ -290,7 +290,7 @@ namespace Hosts.UITests // Delete all existing host-override rules foreach (var deleteBtn in this.FindAll /// The path to the image file. - /// The fuzz factor for color comparison, default is 5. - public static void EraseUserPreferenceColor(string imagePath, int fuzz = 5) + /// 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(); @@ -158,9 +158,9 @@ namespace Microsoft.PowerToys.UITest /// /// The first color. /// The second color. - /// The fuzz factor, default is 5. + /// 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 = 5) + public static bool HueIsSame(Color c1, Color c2, int fuzz = 2) { var h1 = GetHSL(c1).H; var h2 = GetHSL(c2).H; From 77a5ef7d32cc1cf790075d7b30d94f8078d1dcf1 Mon Sep 17 00:00:00 2001 From: "Xiaofeng Wang (from Dev Box)" Date: Fri, 14 Mar 2025 12:13:22 +0800 Subject: [PATCH 21/43] Check window in RemoveAllEntries --- src/modules/Hosts/Hosts.UITests/HostModuleTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs b/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs index fd281d667a..f2ca4f9321 100644 --- a/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs +++ b/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs @@ -290,8 +290,9 @@ namespace Hosts.UITests // Delete all existing host-override rules foreach (var deleteBtn in this.FindAll public class Session { - private WindowsDriver Root { get; set; } + public WindowsDriver Root { get; set; } private WindowsDriver WindowsDriver { get; set; } diff --git a/src/common/UITestAutomation/UITestBase.cs b/src/common/UITestAutomation/UITestBase.cs index 88aeb07e86..494f622be5 100644 --- a/src/common/UITestAutomation/UITestBase.cs +++ b/src/common/UITestAutomation/UITestBase.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; @@ -18,6 +19,8 @@ namespace Microsoft.PowerToys.UITest [TestClass] public class UITestBase { + public required TestContext TestContext { get; set; } + public required Session Session { get; set; } private readonly PowerToysModule scope; @@ -256,5 +259,30 @@ namespace Microsoft.PowerToys.UITest { return this.Session.FindAll(By.Name(name), timeoutMS); } + + protected void AttachmentWrapper(Action action) + { + try + { + action(); + } + catch (Exception) + { + this.CaptureScreenshot(); + throw; + } + } + + protected void CaptureScreenshot() + { + // 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, "screenshot.png"); + + this.Session.Root.GetScreenshot().SaveAsFile(screenshotPath, ScreenshotImageFormat.Png); + + // Save screenshot to screenshotPath & upload to test attachment + this.TestContext.AddResultFile(screenshotPath); + } } } diff --git a/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs b/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs index 5098cea1d1..fbdc92fbe7 100644 --- a/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs +++ b/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs @@ -34,24 +34,27 @@ namespace Hosts.UITests [TestMethod("Hosts.Basic.EmptyViewShouldWork")] public void TestEmptyView() { - this.CloseWarningDialog(); - this.RemoveAllEntries(); + this.AttachmentWrapper(() => + { + this.CloseWarningDialog(); + this.RemoveAllEntries(); - // 'Add an entry' button (only show-up when list is empty) should be visible - Assert.IsTrue(this.HasOne("Add an entry"), "'Add an entry' button should be visible in the empty view"); + // 'Add an entry' button (only show-up when list is empty) should be visible + Assert.IsTrue(this.HasOne("Add an entry1"), "'Add an entry' button should be visible in the empty view"); - // VisualAssert.AreEqual(this.Find("Entries"), "EmptyView"); + // VisualAssert.AreEqual(this.Find("Entries"), "EmptyView"); - // Click 'Add an entry' from empty-view for adding Host override rule - this.Find("Add an entry").Click(); + // Click 'Add an entry' from empty-view for adding Host override rule + this.Find("Add an entry").Click(); - this.AddEntry("192.168.0.1", "localhost", false, false); + this.AddEntry("192.168.0.1", "localhost", false, false); - // Should have one row now and not more empty view - Assert.IsTrue(this.Has /// 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); @@ -117,9 +117,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); } @@ -128,9 +128,9 @@ 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); } @@ -140,9 +140,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). /// True if only has one element, otherwise false. - public bool HasOne(By by, int timeoutMS = 3000) + public bool HasOne(By by, int timeoutMS = 5000) where T : Element, new() { return this.FindAll(by, timeoutMS).Count == 1; @@ -152,9 +152,9 @@ namespace Microsoft.PowerToys.UITest /// Shortcut for this.HasOne(by, timeoutMS) /// /// The name of the element. - /// The timeout in milliseconds (default is 3000). + /// The timeout in milliseconds (default is 5000). /// True if only has one element, otherwise false. - public bool HasOne(By by, int timeoutMS = 3000) + public bool HasOne(By by, int timeoutMS = 5000) { return this.HasOne(by, timeoutMS); } @@ -164,9 +164,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). /// True if only has one element, otherwise false. - public bool HasOne(string name, int timeoutMS = 3000) + public bool HasOne(string name, int timeoutMS = 5000) where T : Element, new() { return this.HasOne(By.Name(name), timeoutMS); @@ -176,9 +176,9 @@ namespace Microsoft.PowerToys.UITest /// Shortcut for this.HasOne(name, timeoutMS) /// /// The name of the element. - /// The timeout in milliseconds (default is 3000). + /// The timeout in milliseconds (default is 5000). /// True if only has one element, otherwise false. - public bool HasOne(string name, int timeoutMS = 3000) + public bool HasOne(string name, int timeoutMS = 5000) { return this.HasOne(By.Name(name), timeoutMS); } @@ -188,9 +188,9 @@ 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). /// True if has one or more element, otherwise false. - public bool Has(By by, int timeoutMS = 3000) + public bool Has(By by, int timeoutMS = 5000) where T : Element, new() { return this.FindAll(by, timeoutMS).Count >= 1; @@ -200,9 +200,9 @@ namespace Microsoft.PowerToys.UITest /// Shortcut for this.Has(by, timeoutMS) /// /// The selector to find the element. - /// The timeout in milliseconds (default is 3000). + /// The timeout in milliseconds (default is 5000). /// True if has one or more element, otherwise false. - public bool Has(By by, int timeoutMS = 3000) + public bool Has(By by, int timeoutMS = 5000) { return this.Has(by, timeoutMS); } @@ -212,9 +212,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). /// True if has one or more element, otherwise false. - public bool Has(string name, int timeoutMS = 3000) + public bool Has(string name, int timeoutMS = 5000) where T : Element, new() { return this.Has(By.Name(name), timeoutMS); @@ -224,9 +224,9 @@ namespace Microsoft.PowerToys.UITest /// Shortcut for this.Has(name, timeoutMS) /// /// The name of the element. - /// The timeout in milliseconds (default is 3000). + /// The timeout in milliseconds (default is 5000). /// True if has one or more element, otherwise false. - public bool Has(string name, int timeoutMS = 3000) + public bool Has(string name, int timeoutMS = 5000) { return this.Has(name, timeoutMS); } @@ -236,9 +236,9 @@ namespace Microsoft.PowerToys.UITest /// /// 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}"); @@ -260,9 +260,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); @@ -273,9 +273,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); } @@ -285,9 +285,9 @@ 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); } @@ -416,9 +416,6 @@ namespace Microsoft.PowerToys.UITest this.windowHandlers.Add(this.MainWindowHandler); - // Set implicit timeout to make element search retry every 500 ms - this.WindowsDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3); - if (size != WindowSize.UnSpecified) { this.SetMainWindowSize(size); diff --git a/src/common/UITestAutomation/SessionHelper.cs b/src/common/UITestAutomation/SessionHelper.cs index 3590c80fbb..2061c97001 100644 --- a/src/common/UITestAutomation/SessionHelper.cs +++ b/src/common/UITestAutomation/SessionHelper.cs @@ -41,9 +41,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); } /// @@ -59,9 +56,6 @@ namespace Microsoft.PowerToys.UITest Assert.IsNotNull(this.Driver, $"Failed to initialize the test environment. Driver is null."); - // Set default timeout to 5 seconds - this.Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5); - return this; } diff --git a/src/common/UITestAutomation/UITestBase.cs b/src/common/UITestAutomation/UITestBase.cs index 494f622be5..288fd42ce1 100644 --- a/src/common/UITestAutomation/UITestBase.cs +++ b/src/common/UITestAutomation/UITestBase.cs @@ -69,9 +69,9 @@ 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); @@ -82,9 +82,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. - 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); @@ -94,9 +94,9 @@ 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); } @@ -105,9 +105,9 @@ namespace Microsoft.PowerToys.UITest /// 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); } @@ -117,9 +117,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). /// True if only has one element, otherwise false. - public bool HasOne(By by, int timeoutMS = 3000) + public bool HasOne(By by, int timeoutMS = 5000) where T : Element, new() { return this.FindAll(by, timeoutMS).Count == 1; @@ -129,9 +129,9 @@ namespace Microsoft.PowerToys.UITest /// Shortcut for this.Session.HasOne(by, timeoutMS) /// /// The name of the element. - /// The timeout in milliseconds (default is 3000). + /// The timeout in milliseconds (default is 5000). /// True if only has one element, otherwise false. - public bool HasOne(By by, int timeoutMS = 3000) + public bool HasOne(By by, int timeoutMS = 5000) { return this.Session.HasOne(by, timeoutMS); } @@ -141,9 +141,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). /// True if only has one element, otherwise false. - public bool HasOne(string name, int timeoutMS = 3000) + public bool HasOne(string name, int timeoutMS = 5000) where T : Element, new() { return this.Session.HasOne(By.Name(name), timeoutMS); @@ -153,9 +153,9 @@ namespace Microsoft.PowerToys.UITest /// Shortcut for this.Session.HasOne(name, timeoutMS) /// /// The name of the element. - /// The timeout in milliseconds (default is 3000). + /// The timeout in milliseconds (default is 5000). /// True if only has one element, otherwise false. - public bool HasOne(string name, int timeoutMS = 3000) + public bool HasOne(string name, int timeoutMS = 5000) { return this.Session.HasOne(name, timeoutMS); } @@ -165,9 +165,9 @@ 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). /// True if has one or more element, otherwise false. - public bool Has(By by, int timeoutMS = 3000) + public bool Has(By by, int timeoutMS = 5000) where T : Element, new() { return this.Session.FindAll(by, timeoutMS).Count >= 1; @@ -177,9 +177,9 @@ namespace Microsoft.PowerToys.UITest /// Shortcut for this.Session.Has(by, timeoutMS) /// /// The selector to find the element. - /// The timeout in milliseconds (default is 3000). + /// The timeout in milliseconds (default is 5000). /// True if has one or more element, otherwise false. - public bool Has(By by, int timeoutMS = 3000) + public bool Has(By by, int timeoutMS = 5000) { return this.Session.Has(by, timeoutMS); } @@ -189,9 +189,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). /// True if has one or more element, otherwise false. - public bool Has(string name, int timeoutMS = 3000) + public bool Has(string name, int timeoutMS = 5000) where T : Element, new() { return this.Session.Has(By.Name(name), timeoutMS); @@ -201,9 +201,9 @@ namespace Microsoft.PowerToys.UITest /// Shortcut for this.Session.Has(name, timeoutMS) /// /// The name of the element. - /// The timeout in milliseconds (default is 3000). + /// The timeout in milliseconds (default is 5000). /// True if has one or more element, otherwise false. - public bool Has(string name, int timeoutMS = 3000) + public bool Has(string name, int timeoutMS = 5000) { return this.Session.Has(name, timeoutMS); } @@ -214,9 +214,9 @@ namespace Microsoft.PowerToys.UITest /// /// 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); @@ -228,9 +228,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); @@ -241,9 +241,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); } @@ -253,9 +253,9 @@ 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); } diff --git a/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs b/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs index fbdc92fbe7..681529c7ef 100644 --- a/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs +++ b/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs @@ -40,7 +40,7 @@ namespace Hosts.UITests this.RemoveAllEntries(); // 'Add an entry' button (only show-up when list is empty) should be visible - Assert.IsTrue(this.HasOne("Add an entry1"), "'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"); @@ -297,10 +297,7 @@ namespace Hosts.UITests foreach (var deleteBtn in this.FindAll