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/Button.cs b/src/common/UITestAutomation/Element/Button.cs index 44a7055fb2..b5915031be 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 { /// diff --git a/src/common/UITestAutomation/Element/By.cs b/src/common/UITestAutomation/Element/By.cs index 11217ff61a..5ccaf1eb06 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. /// diff --git a/src/common/UITestAutomation/Element/Element.cs b/src/common/UITestAutomation/Element/Element.cs index d4794507d6..f2f3d4ee29 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,19 @@ 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); + + // Move 2by2 offset to make click more stable instead of click on the border of the element + actions.MoveByOffset(2, 2); + if (rightClick) { actions.ContextClick(); @@ -106,6 +108,25 @@ namespace Microsoft.PowerToys.UITest { actions.Click(); } + + actions.Build().Perform(); + }); + } + + /// + /// Double Click the UI element. + /// + public void DoubleClick() + { + 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(2, 2); + + actions.DoubleClick(); + actions.Build().Perform(); }); } @@ -133,7 +154,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 +178,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 +194,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(); + } } } } diff --git a/src/common/UITestAutomation/Element/TextBox.cs b/src/common/UITestAutomation/Element/TextBox.cs new file mode 100644 index 0000000000..e427f7c9d3 --- /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; + } + } +} diff --git a/src/common/UITestAutomation/Element/ToggleSwitch.cs b/src/common/UITestAutomation/Element/ToggleSwitch.cs new file mode 100644 index 0000000000..b2a7f1b607 --- /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 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; + } + } +} diff --git a/src/common/UITestAutomation/Element/Window.cs b/src/common/UITestAutomation/Element/Window.cs index 558e0cb0b2..eceb1fcf47 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 { /// 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..241aa6b339 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,7 +44,12 @@ 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; diff --git a/src/common/UITestAutomation/Session.cs b/src/common/UITestAutomation/Session.cs index 2e21cc392e..7071f6d2e2 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); 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..4ce432f23a 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. ///