Update UI-Test Automation Framework (#37597)

* Improve UITest Automation

* Improve UITest Automation

* Exclude all UI-Test projects instead of just fancyZone UITest

* Exclude all UI-Test projects instead of just fancyZone UITest

* Fix code-style
This commit is contained in:
Jerry Xu 2025-02-24 02:05:55 -08:00 committed by GitHub
parent 3970e89ee7
commit 83cea39b66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 190 additions and 71 deletions

View File

@ -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

View File

@ -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
{
/// <summary>

View File

@ -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();
}
/// <summary>
/// Creates a By object using the name attribute.
/// </summary>

View File

@ -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<WindowsElement>? driver;
internal void SetWindowsElement(WindowsElement windowsElement) => this.windowsElement = windowsElement;
@ -43,7 +39,20 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
public string Text
{
get { return GetAttribute("Value"); }
get { return this.windowsElement?.Text ?? string.Empty; }
}
/// <summary>
/// Gets a value indicating whether the UI element is Enabled or not.
/// </summary>
public bool Enabled
{
get { return this.windowsElement?.Enabled ?? false; }
}
public bool Selected
{
get { return this.windowsElement?.Selected ?? false; }
}
/// <summary>
@ -78,26 +87,19 @@ namespace Microsoft.PowerToys.UITest
get { return GetAttribute("ControlType"); }
}
/// <summary>
/// Checks if the UI element is enabled.
/// </summary>
/// <returns>True if the element is enabled; otherwise, false.</returns>
public bool IsEnabled() => GetAttribute("IsEnabled") == "True";
/// <summary>
/// Checks if the UI element is selected.
/// </summary>
/// <returns>True if the element is selected; otherwise, false.</returns>
public bool IsSelected() => GetAttribute("IsSelected") == "True";
/// <summary>
/// Click the UI element.
/// </summary>
/// <param name="rightClick">If true, performs a right-click; otherwise, performs a left-click.</param>
/// <param name="rightClick">If true, performs a right-click; otherwise, performs a left-click. Default value is false</param>
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();
});
}
/// <summary>
/// Double Click the UI element.
/// </summary>
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<T, AppiumWebElement>(
var foundElement = FindHelper.Find<T, AppiumWebElement>(
() =>
{
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<T, AppiumWebElement>(
var foundElements = FindHelper.FindAll<T, AppiumWebElement>(
() =>
{
var elements = this.windowsElement.FindElements(by.ToSeleniumBy());
@ -173,13 +194,24 @@ namespace Microsoft.PowerToys.UITest
/// <summary>
/// Simulates a manual operation on the element.
/// </summary>
private void PerformAction(Action<Actions> action)
/// <param name="action">The action to perform on the element.</param>
/// <param name="msPreAction">The number of milliseconds to wait before the action. Default value is 100 ms</param>
/// <param name="msPostAction">The number of milliseconds to wait after the action. Default value is 100 ms</param>
protected void PerformAction(Action<Actions, WindowsElement> 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();
}
}
}
}

View File

@ -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
{
/// <summary>
/// Represents a textbox in the UI test environment.
/// </summary>
public class TextBox : Element
{
/// <summary>
/// Sets the text of the textbox.
/// </summary>
/// <param name="value">The text to set.</param>
/// <param name="clearText">A value indicating whether to clear the text before setting it. Default value is true</param>
/// <returns>The current TextBox instance.</returns>
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;
}
}
}

View File

@ -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
{
/// <summary>
/// Represents a ToggleSwitch in the UI test environment.
/// </summary>
public class ToggleSwitch : Button
{
/// <summary>
/// Gets a value indicating whether the ToggleSwitch is on.
/// </summary>
public bool IsOn
{
get
{
return this.Selected;
}
}
/// <summary>
/// Sets the ToggleSwitch to the specified value.
/// </summary>
/// <param name="value">A value indicating whether the ToggleSwitch should be active. Default is true</param>
/// <returns>The current ToggleSwitch instance.</returns>
public ToggleSwitch Toggle(bool value = true)
{
if (this.IsOn != value)
{
// Toggle the switch
this.Click();
}
return this;
}
}
}

View File

@ -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
{
/// <summary>

View File

@ -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
/// <summary>
/// Helper class for finding elements.
/// </summary>
internal static class FindElementHelper
internal static class FindHelper
{
public static T Find<T, TW>(Func<TW> findElementFunc, WindowsDriver<WindowsElement>? 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();
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;

View File

@ -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<T, WindowsElement>(
var foundElement = FindHelper.Find<T, WindowsElement>(
() =>
{
var element = this.WindowsDriver.FindElement(by.ToSeleniumBy());
@ -65,21 +59,20 @@ namespace Microsoft.PowerToys.UITest
/// <param name="by">The selector to find the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<T>? FindAll<T>(By by, int timeoutMS = 3000)
public ReadOnlyCollection<T> FindAll<T>(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<T, WindowsElement>(
var foundElements = FindHelper.FindAll<T, WindowsElement>(
() =>
{
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<T>(new List<T>());
}
/// <summary>
@ -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<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), appCapabilities);

View File

@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
@ -15,7 +16,4 @@
<PackageReference Include="System.IO.Abstractions" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
</Project>

View File

@ -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();
}
/// <summary>
/// Finds an element by selector.
/// Shortcut for this.Session.Find<T>(by, timeoutMS)
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="by">The selector to find the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
protected T Find<T>(By by, int timeoutMS = 3000)
where T : Element, new()
{
return this.Session.Find<T>(by, timeoutMS);
}
/// <summary>
/// Finds all elements by selector.
/// Shortcut for this.Session.FindAll<T>(by, timeoutMS)
/// </summary>
/// <typeparam name="T">The class of the elements, should be Element or its derived class.</typeparam>
/// <param name="by">The selector to find the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
protected ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 3000)
where T : Element, new()
{
return this.Session.FindAll<T>(by, timeoutMS);
}
/// <summary>
/// Nested class for test initialization.
/// </summary>