From fd64f5dadb1d22112bc8f31f9703e98389658a2f Mon Sep 17 00:00:00 2001 From: Jerry Xu Date: Wed, 26 Feb 2025 19:17:07 +0800 Subject: [PATCH] Start to convert manual test-case to automation --- src/common/UITestAutomation/Element/Button.cs | 9 ++ .../UITestAutomation/Element/Element.cs | 113 +++++++++++++++--- .../Element/HyperlinkButton.cs | 23 ++++ .../Element/NavigationViewItem.cs | 59 +++++++++ .../UITestAutomation/Element/TextBlock.cs | 23 ++++ .../UITestAutomation/Element/TextBox.cs | 13 +- src/common/UITestAutomation/Element/Window.cs | 8 +- src/common/UITestAutomation/FindHelper.cs | 9 +- src/common/UITestAutomation/Session.cs | 92 ++++++++++++-- src/common/UITestAutomation/SessionHelper.cs | 9 +- src/common/UITestAutomation/UITestBase.cs | 98 ++++++++++++++- .../Hosts/Hosts.UITests/HostModuleTests.cs | 72 +++++++---- .../Hosts/Hosts.UITests/HostsSettingTests.cs | 85 +++++++++++++ .../Hosts.UITests/Release-Test-Checklist.md | 26 ++++ 14 files changed, 568 insertions(+), 71 deletions(-) create mode 100644 src/common/UITestAutomation/Element/HyperlinkButton.cs create mode 100644 src/common/UITestAutomation/Element/NavigationViewItem.cs create mode 100644 src/common/UITestAutomation/Element/TextBlock.cs create mode 100644 src/modules/Hosts/Hosts.UITests/HostsSettingTests.cs create mode 100644 src/modules/Hosts/Hosts.UITests/Release-Test-Checklist.md diff --git a/src/common/UITestAutomation/Element/Button.cs b/src/common/UITestAutomation/Element/Button.cs index b5915031be92..8225971b4e32 100644 --- a/src/common/UITestAutomation/Element/Button.cs +++ b/src/common/UITestAutomation/Element/Button.cs @@ -9,5 +9,14 @@ namespace Microsoft.PowerToys.UITest /// public class Button : Element { + private static readonly string ExpectedControlType = "ControlType.Button"; + + /// + /// Initializes a new instance of the class. + /// + public Button() + { + this.TargetControlType = Button.ExpectedControlType; + } } } diff --git a/src/common/UITestAutomation/Element/Element.cs b/src/common/UITestAutomation/Element/Element.cs index 7d78ac5f9c4e..59c799e401dc 100644 --- a/src/common/UITestAutomation/Element/Element.cs +++ b/src/common/UITestAutomation/Element/Element.cs @@ -22,6 +22,14 @@ public class Element private WindowsDriver? driver; + protected string? TargetControlType { get; set; } + + internal bool IsMatchingTarget() + { + var ct = this.ControlType; + return string.IsNullOrEmpty(this.TargetControlType) || this.TargetControlType == this.ControlType; + } + internal void SetWindowsElement(WindowsElement windowsElement) => this.windowsElement = windowsElement; internal void SetSession(WindowsDriver driver) => this.driver = driver; @@ -91,7 +99,7 @@ public string ControlType /// Click the UI element. /// /// If true, performs a right-click; otherwise, performs a left-click. Default value is false - public void Click(bool rightClick = false) + public virtual void Click(bool rightClick = false) { PerformAction((actions, windowElement) => { @@ -116,7 +124,7 @@ public void Click(bool rightClick = false) /// /// Double Click the UI element. /// - public void DoubleClick() + public virtual void DoubleClick() { PerformAction((actions, windowElement) => { @@ -139,7 +147,6 @@ public string GetAttribute(string attributeName) { Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method GetAttribute with parameter: attributeName = {attributeName}"); var attributeValue = this.windowsElement.GetAttribute(attributeName); - Assert.IsNotNull(attributeValue, $"Attribute '{attributeName}' is null."); return attributeValue; } @@ -154,17 +161,51 @@ public T Find(By by, int timeoutMS = 3000) 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 = FindHelper.Find( - () => - { - var element = this.windowsElement.FindElement(by.ToSeleniumBy()); - Assert.IsNotNull(element, $"Element not found using selector: {by}"); - return element; - }, - this.driver, - timeoutMS); - return foundElement; + // leverage findAll to filter out mismatched elements + var collection = this.FindAll(by, timeoutMS); + + Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}"); + + 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) + /// + /// The name for finding the element. + /// The timeout in milliseconds. + /// The found element. + public Element Find(string name, int timeoutMS = 3000) + { + return this.Find(By.Name(name), timeoutMS); } /// @@ -187,16 +228,54 @@ public ReadOnlyCollection FindAll(By by, int timeoutMS = 3000) this.driver, timeoutMS); - return foundElements ?? new ReadOnlyCollection(new List()); + return foundElements ?? new ReadOnlyCollection([]); + } + + /// + /// Finds all elements by the selector. + /// Shortcut for this.FindAll(By.Name(name), timeoutMS) + /// + /// The class type of the elements to find. + /// The name for finding the element. + /// The timeout in milliseconds. + /// A read-only collection of the found elements. + public ReadOnlyCollection FindAll(string name, int timeoutMS = 3000) + where T : Element, new() + { + return this.FindAll(By.Name(name), timeoutMS); + } + + /// + /// Finds all elements by the selector. + /// Shortcut for this.FindAll(by, timeoutMS) + /// + /// The selector to use for finding the elements. + /// The timeout in milliseconds. + /// A read-only collection of the found elements. + public ReadOnlyCollection FindAll(By by, int timeoutMS = 3000) + { + return this.FindAll(by, timeoutMS); + } + + /// + /// Finds all elements by the selector. + /// Shortcut for this.FindAll(By.Name(name), timeoutMS) + /// + /// The name for finding the element. + /// The timeout in milliseconds. + /// A read-only collection of the found elements. + public ReadOnlyCollection FindAll(string name, int timeoutMS = 3000) + { + return this.FindAll(By.Name(name), timeoutMS); } /// /// Simulates a manual operation on the element. /// /// The action to perform on the element. - /// The number of milliseconds to wait before the action. Default value is 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) + /// The number of milliseconds to wait before the action. Default value is 500 ms + /// The number of milliseconds to wait after the action. Default value is 500 ms + protected void PerformAction(Action action, int msPreAction = 500, int msPostAction = 500) { if (msPreAction > 0) { diff --git a/src/common/UITestAutomation/Element/HyperlinkButton.cs b/src/common/UITestAutomation/Element/HyperlinkButton.cs new file mode 100644 index 000000000000..738414b1b508 --- /dev/null +++ b/src/common/UITestAutomation/Element/HyperlinkButton.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.PowerToys.UITest +{ + /// + /// Represents a HyperLinkButton in the UI test environment. + /// HyperLinkButton reepresents a button control that functions as a hyperlink. + /// + public class HyperlinkButton : Button + { + private static readonly string ExpectedControlType = "ControlType.HyperLink"; + + /// + /// Initializes a new instance of the class. + /// + public HyperlinkButton() + { + this.TargetControlType = HyperlinkButton.ExpectedControlType; + } + } +} diff --git a/src/common/UITestAutomation/Element/NavigationViewItem.cs b/src/common/UITestAutomation/Element/NavigationViewItem.cs new file mode 100644 index 000000000000..c23713de31a4 --- /dev/null +++ b/src/common/UITestAutomation/Element/NavigationViewItem.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.PowerToys.UITest +{ + /// + /// Represents a NavigationViewItem in the UI test environment. + /// NavigationViewItem pepresents the container for an item in a NavigationView control. + /// + public class NavigationViewItem : Element + { + private static readonly string ExpectedControlType = "ControlType.ListItem"; + + /// + /// Initializes a new instance of the class. + /// + public NavigationViewItem() + { + this.TargetControlType = NavigationViewItem.ExpectedControlType; + } + + /// + /// Click the ListItem element. + /// + /// If true, performs a right-click; otherwise, performs a left-click. Default value is false + public override void Click(bool rightClick = false) + { + PerformAction((actions, windowElement) => + { + actions.MoveToElement(windowElement, 10, 10); + + if (rightClick) + { + actions.ContextClick(); + } + else + { + actions.Click(); + } + + actions.Build().Perform(); + }); + } + + /// + /// Double Click the ListItem element. + /// + public override void DoubleClick() + { + PerformAction((actions, windowElement) => + { + actions.MoveToElement(windowElement, 10, 10); + actions.DoubleClick(); + actions.Build().Perform(); + }); + } + } +} diff --git a/src/common/UITestAutomation/Element/TextBlock.cs b/src/common/UITestAutomation/Element/TextBlock.cs new file mode 100644 index 000000000000..883b104e2623 --- /dev/null +++ b/src/common/UITestAutomation/Element/TextBlock.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.PowerToys.UITest +{ + /// + /// Represents a TextBlock in the UI test environment. + /// TextBlock provides a lightweight control for displaying small amounts of flow content. + /// + public class TextBlock : Element + { + private static readonly string ExpectedControlType = "ControlType.Text"; + + /// + /// Initializes a new instance of the class. + /// + public TextBlock() + { + this.TargetControlType = TextBlock.ExpectedControlType; + } + } +} diff --git a/src/common/UITestAutomation/Element/TextBox.cs b/src/common/UITestAutomation/Element/TextBox.cs index e427f7c9d316..dc6d70fe38cc 100644 --- a/src/common/UITestAutomation/Element/TextBox.cs +++ b/src/common/UITestAutomation/Element/TextBox.cs @@ -7,10 +7,21 @@ namespace Microsoft.PowerToys.UITest { /// - /// Represents a textbox in the UI test environment. + /// Represents a TextBox in the UI test environment. + /// TextBox rpresents a control that can be used to display and edit plain text (single or multi-line). /// public class TextBox : Element { + private static readonly string ExpectedControlType = "ControlType.Edit"; + + /// + /// Initializes a new instance of the class. + /// + public TextBox() + { + this.TargetControlType = TextBox.ExpectedControlType; + } + /// /// Sets the text of the textbox. /// diff --git a/src/common/UITestAutomation/Element/Window.cs b/src/common/UITestAutomation/Element/Window.cs index eceb1fcf4793..5fc5fc6e266c 100644 --- a/src/common/UITestAutomation/Element/Window.cs +++ b/src/common/UITestAutomation/Element/Window.cs @@ -18,7 +18,7 @@ public Window Maximize(bool byClickButton = true) { if (byClickButton) { - Find internal static class FindHelper { - public static T Find(Func findElementFunc, WindowsDriver? driver, int timeoutMS) - where T : Element, new() - { - var item = findElementFunc() as WindowsElement; - return NewElement(item, driver, timeoutMS); - } - public static ReadOnlyCollection? FindAll(Func> findElementsFunc, WindowsDriver? driver, int timeoutMS) where T : Element, new() { @@ -32,7 +25,7 @@ public static T Find(Func findElementFunc, WindowsDriver(element, driver, timeoutMS); - }).ToList(); + }).Where(item => item.IsMatchingTarget()).ToList(); return new ReadOnlyCollection(res); } diff --git a/src/common/UITestAutomation/Session.cs b/src/common/UITestAutomation/Session.cs index 7071f6d2e236..ef0a6fff3fad 100644 --- a/src/common/UITestAutomation/Session.cs +++ b/src/common/UITestAutomation/Session.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Runtime.InteropServices; +using System.Xml.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; @@ -39,17 +40,48 @@ public T Find(By by, int timeoutMS = 3000) 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 = FindHelper.Find( - () => - { - var element = this.WindowsDriver.FindElement(by.ToSeleniumBy()); - Assert.IsNotNull(element, $"Element not found using selector: {by}"); - return element; - }, - this.WindowsDriver, - timeoutMS); - return foundElement; + // leverage findAll to filter out mismatched elements + var collection = this.FindAll(by, timeoutMS); + + Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}"); + + return collection[0]; + } + + /// + /// Shortcut for this.Find(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). + /// The found element. + public T Find(string name, int timeoutMS = 3000) + where T : Element, new() + { + return this.Find(By.Name(name), timeoutMS); + } + + /// + /// Shortcut for this.Find(by, timeoutMS) + /// + /// The selector to find the element. + /// The timeout in milliseconds (default is 3000). + /// The found element. + public Element Find(By by, int timeoutMS = 3000) + { + return this.Find(by, timeoutMS); + } + + /// + /// Shortcut for this.Find(By.Name(name), timeoutMS) + /// + /// The name of the element. + /// The timeout in milliseconds (default is 3000). + /// The found element. + public Element Find(string name, int timeoutMS = 3000) + { + return this.Find(By.Name(name), timeoutMS); } /// @@ -72,7 +104,45 @@ public ReadOnlyCollection FindAll(By by, int timeoutMS = 3000) this.WindowsDriver, timeoutMS); - return foundElements ?? new ReadOnlyCollection(new List()); + return foundElements ?? new ReadOnlyCollection([]); + } + + /// + /// Finds all elements by selector. + /// Shortcut for this.FindAll(By.Name(name), timeoutMS) + /// + /// 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). + /// A read-only collection of the found elements. + public ReadOnlyCollection FindAll(string name, int timeoutMS = 3000) + where T : Element, new() + { + return this.FindAll(By.Name(name), timeoutMS); + } + + /// + /// Finds all elements by selector. + /// Shortcut for this.FindAll(by, timeoutMS) + /// + /// 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) + { + return this.FindAll(by, timeoutMS); + } + + /// + /// Finds all elements by selector. + /// Shortcut for this.FindAll(By.Name(name), timeoutMS) + /// + /// The name to find the elements. + /// The timeout in milliseconds (default is 3000). + /// A read-only collection of the found elements. + public ReadOnlyCollection FindAll(string name, int timeoutMS = 3000) + { + return this.FindAll(By.Name(name), timeoutMS); } /// diff --git a/src/common/UITestAutomation/SessionHelper.cs b/src/common/UITestAutomation/SessionHelper.cs index ec2e97caec76..7bb1f6e7a6fd 100644 --- a/src/common/UITestAutomation/SessionHelper.cs +++ b/src/common/UITestAutomation/SessionHelper.cs @@ -25,21 +25,16 @@ internal class SessionHelper private Process? appDriver; - public SessionHelper(PowerToysModule scope, bool runAsAdmin) + public SessionHelper(PowerToysModule scope) { this.sessionPath = ModuleConfigData.Instance.GetModulePath(scope); var winAppDriverProcessInfo = new ProcessStartInfo { FileName = "C:\\Program Files (x86)\\Windows Application Driver\\WinAppDriver.exe", + Verb = "runas", }; - if (runAsAdmin) - { - winAppDriverProcessInfo.UseShellExecute = true; - winAppDriverProcessInfo.Verb = "runas"; - } - this.appDriver = Process.Start(winAppDriverProcessInfo); var desktopCapabilities = new AppiumOptions(); diff --git a/src/common/UITestAutomation/UITestBase.cs b/src/common/UITestAutomation/UITestBase.cs index 2fbb6cefe827..1d6502ac5447 100644 --- a/src/common/UITestAutomation/UITestBase.cs +++ b/src/common/UITestAutomation/UITestBase.cs @@ -15,15 +15,19 @@ namespace Microsoft.PowerToys.UITest /// /// Base class that should be inherited by all Test Classes. /// + [TestClass] public class UITestBase { public Session Session { get; set; } private readonly SessionHelper sessionHelper; - public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, bool runAsAdmin = false) + private readonly PowerToysModule scope; + + public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings) { - this.sessionHelper = new SessionHelper(scope, runAsAdmin).Init(); + this.scope = scope; + this.sessionHelper = new SessionHelper(scope).Init(); this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver()); } @@ -32,6 +36,23 @@ public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, boo this.sessionHelper.Cleanup(); } + /// + /// Initializes the test. + /// + [TestInitialize] + public void TestInit() + { + if (this.scope == PowerToysModule.PowerToysSettings) + { + // close Debug warning dialog if any + // Such debug warning dialog seems only appear in PowerToys Settings + if (this.FindAll("DEBUG").Count > 0) + { + this.Find("DEBUG").Find