Dev/zhaopengwang/test/37733 UI test fancyzones editor (#37769)

* first launch test

* add FancyZonesEditorHelper

* click monitor test and add FindByAccessibilityId function

* add ui initialization tests and add exit scope exe function

* add cleanup test function and change file init

* add TemplateLayoutsTest and add LayoutTypeEnumExtension.cs and Element class add sendkey function

* add UI Initialize Test

* add OpenEditLayoutDialog test case and add By type

* add LayoutHotkeysTest

* add EditLayoutTests and add element drag function

* add DeleteLayouTest and change cleanup to base class and change FindByAccessibilityId to By.AccessibilityId

* add DefaultLayoutsTest

* add CustomLayoutsTest

* add CreateLayoutTest

* add CopyLayoutTest

* add ApplyLayoutTest

* add some cleanup code

* fix spelling error

* fix DeleteLayoutWithHotkey test code bug

* change code

* fix restart exe some bug

* move first lunch text code to new file

* test write file error

* fix test code init fancyzone file error

* test maxsize button

* get current window size

* change layout count

* change test case work windows size

* change fancyzone editor window size

* change fancyzone editor window size and change element move rule

* change window size

---------

Co-authored-by: Zhaopeng Wang <zhaopengwang@microsoft.com>
This commit is contained in:
dreamstart
2025-03-14 17:04:23 +08:00
committed by GitHub
parent 39073f0467
commit 53f8499434
27 changed files with 5695 additions and 72 deletions

View File

@@ -11,17 +11,40 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
public class By
{
private readonly OpenQA.Selenium.By by;
private readonly OpenQA.Selenium.By? by;
private readonly bool isAccessibilityId;
private readonly string? accessibilityId;
private By(OpenQA.Selenium.By by)
{
isAccessibilityId = false;
this.by = by;
}
private By(string accessibilityId)
{
isAccessibilityId = true;
this.accessibilityId = accessibilityId;
}
public override string ToString()
{
// override ToString to return detailed debugging content provided by OpenQA.Selenium.By
return this.by.ToString();
return this.GetAccessibilityId();
}
public bool GetIsAccessibilityId() => this.isAccessibilityId;
public string GetAccessibilityId()
{
if (this.isAccessibilityId)
{
return this.accessibilityId!;
}
else
{
return this.by!.ToString();
}
}
/// <summary>
@@ -31,6 +54,13 @@ namespace Microsoft.PowerToys.UITest
/// <returns>A By object.</returns>
public static By Name(string name) => new By(OpenQA.Selenium.By.Name(name));
/// <summary>
/// Creates a By object using the className attribute.
/// </summary>
/// <param name="className">The className attribute to search for.</param>
/// <returns>A By object.</returns>
public static By ClassName(string className) => new By(OpenQA.Selenium.By.ClassName(className));
/// <summary>
/// Creates a By object using the ID attribute.
/// </summary>
@@ -38,6 +68,13 @@ namespace Microsoft.PowerToys.UITest
/// <returns>A By object.</returns>
public static By Id(string id) => new By(OpenQA.Selenium.By.Id(id));
/// <summary>
/// Creates a By object using the ID attribute.
/// </summary>
/// <param name="accessibilityId">The ID attribute to search for.</param>
/// <returns>A By object.</returns>
public static By AccessibilityId(string accessibilityId) => new By(accessibilityId);
/// <summary>
/// Creates a By object using the XPath expression.
/// </summary>
@@ -70,6 +107,6 @@ namespace Microsoft.PowerToys.UITest
/// Converts the By object to an OpenQA.Selenium.By object.
/// </summary>
/// <returns>An OpenQA.Selenium.By object.</returns>
internal OpenQA.Selenium.By ToSeleniumBy() => by;
internal OpenQA.Selenium.By ToSeleniumBy() => by!;
}
}

View File

@@ -3,8 +3,13 @@
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Xml.Linq;
using ABI.Windows.Foundation;
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
@@ -63,6 +68,14 @@ namespace Microsoft.PowerToys.UITest
get { return this.windowsElement?.Selected ?? false; }
}
/// <summary>
/// Gets the Rect of the UI element.
/// </summary>
public Rectangle? Rect
{
get { return this.windowsElement?.Rect; }
}
/// <summary>
/// Gets the AutomationID of the UI element.
/// </summary>
@@ -138,6 +151,54 @@ namespace Microsoft.PowerToys.UITest
});
}
/// <summary>
/// Drag element move offset.
/// </summary>
/// <param name="offsetX">The offsetX to move.</param>
/// <param name="offsetY">The offsetY to move.</param>
public void Drag(int offsetX, int offsetY)
{
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement).MoveByOffset(10, 10).ClickAndHold(windowElement).MoveByOffset(offsetX, offsetY).Release();
actions.Build().Perform();
});
}
/// <summary>
/// Drag element move to other element.
/// </summary>
/// <param name="element">Move to this element.</param>
public void Drag(Element element)
{
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement).ClickAndHold();
Assert.IsNotNull(element.windowsElement, "element is null");
int dx = (element.windowsElement.Rect.X - windowElement.Rect.X) / 10;
int dy = (element.windowsElement.Rect.Y - windowElement.Rect.Y) / 10;
for (int i = 0; i < 10; i++)
{
actions.MoveByOffset(dx, dy);
}
actions.Release();
actions.Build().Perform();
});
}
/// <summary>
/// Send Key of the element.
/// </summary>
/// <param name="key">The Key to Send.</param>
public void SendKeys(string key)
{
PerformAction((actions, windowElement) =>
{
windowElement.SendKeys(key);
});
}
/// <summary>
/// Gets the attribute value of the UI element.
/// </summary>
@@ -222,8 +283,16 @@ namespace Microsoft.PowerToys.UITest
var foundElements = FindHelper.FindAll<T, AppiumWebElement>(
() =>
{
var elements = this.windowsElement.FindElements(by.ToSeleniumBy());
return elements;
if (by.GetIsAccessibilityId())
{
var elements = this.windowsElement.FindElementsByAccessibilityId(by.GetAccessibilityId());
return elements;
}
else
{
var elements = this.windowsElement.FindElements(by.ToSeleniumBy());
return elements;
}
},
this.driver,
timeoutMS);

View File

@@ -17,6 +17,19 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
internal static class FindHelper
{
public static ReadOnlyCollection<T>? FindAll<T, TW>(Func<IReadOnlyCollection<TW>> findElementsFunc, WindowsDriver<WindowsElement>? driver, int timeoutMS)
where T : Element, new()
{
var items = findElementsFunc();
var res = items.Select(item =>
{
var element = item as WindowsElement;
return NewElement<T>(element, driver, timeoutMS);
}).Where(item => item.IsMatchingTarget()).ToList();
return new ReadOnlyCollection<T>(res);
}
public static ReadOnlyCollection<T>? FindAll<T, TW>(Func<ReadOnlyCollection<TW>> findElementsFunc, WindowsDriver<WindowsElement>? driver, int timeoutMS)
where T : Element, new()
{

View File

@@ -8,6 +8,7 @@ using System.Xml.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
namespace Microsoft.PowerToys.UITest
{
@@ -98,8 +99,16 @@ namespace Microsoft.PowerToys.UITest
var foundElements = FindHelper.FindAll<T, WindowsElement>(
() =>
{
var elements = this.WindowsDriver.FindElements(by.ToSeleniumBy());
return elements;
if (by.GetIsAccessibilityId())
{
var elements = this.WindowsDriver.FindElementsByAccessibilityId(by.GetAccessibilityId());
return elements;
}
else
{
var elements = this.WindowsDriver.FindElements(by.ToSeleniumBy());
return elements;
}
},
this.WindowsDriver,
timeoutMS);
@@ -145,6 +154,39 @@ namespace Microsoft.PowerToys.UITest
return this.FindAll<Element>(By.Name(name), timeoutMS);
}
/// <summary>
/// Keyboard Action key.
/// </summary>
/// <param name="key1">The Keys1 to click.</param>
/// <param name="key2">The Keys2 to click.</param>
/// <param name="key3">The Keys3 to click.</param>
/// <param name="key4">The Keys4 to click.</param>
public void KeyboardAction(string key1, string key2 = "", string key3 = "", string key4 = "")
{
PerformAction((actions, windowElement) =>
{
if (string.IsNullOrEmpty(key2))
{
actions.SendKeys(key1);
}
else if (string.IsNullOrEmpty(key3))
{
actions.SendKeys(key1).SendKeys(key2);
}
else if (string.IsNullOrEmpty(key4))
{
actions.SendKeys(key1).SendKeys(key2).SendKeys(key3);
}
else
{
actions.SendKeys(key1).SendKeys(key2).SendKeys(key3).SendKeys(key4);
}
actions.Release();
actions.Build().Perform();
});
}
/// <summary>
/// Attaches to an existing PowerToys module.
/// </summary>
@@ -189,5 +231,28 @@ namespace Microsoft.PowerToys.UITest
return this;
}
/// <summary>
/// Simulates a manual operation on the element.
/// </summary>
/// <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 500 ms</param>
/// <param name="msPostAction">The number of milliseconds to wait after the action. Default value is 500 ms</param>
protected void PerformAction(Action<Actions, WindowsDriver<WindowsElement>> action, int msPreAction = 500, int msPostAction = 500)
{
if (msPreAction > 0)
{
Task.Delay(msPreAction).Wait();
}
var windowsDriver = this.WindowsDriver;
Actions actions = new Actions(this.WindowsDriver);
action(actions, windowsDriver);
if (msPostAction > 0)
{
Task.Delay(msPostAction).Wait();
}
}
}
}

View File

@@ -2,6 +2,7 @@
// 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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
@@ -19,15 +20,19 @@ namespace Microsoft.PowerToys.UITest
// Default session path is PowerToys settings dashboard
private readonly string sessionPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.PowerToysSettings);
private string? locationPath;
private WindowsDriver<WindowsElement> Root { get; set; }
private WindowsDriver<WindowsElement>? Driver { get; set; }
private Process? appDriver;
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "<Pending>")]
public SessionHelper(PowerToysModule scope)
{
this.sessionPath = ModuleConfigData.Instance.GetModulePath(scope);
this.locationPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var winAppDriverProcessInfo = new ProcessStartInfo
{
@@ -49,17 +54,12 @@ namespace Microsoft.PowerToys.UITest
/// Initializes the test environment.
/// </summary>
/// <param name="scope">The PowerToys module to start.</param>
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "<Pending>")]
public SessionHelper Init()
{
string? path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
this.StartExe(path + this.sessionPath);
this.StartExe(locationPath + this.sessionPath);
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;
}
@@ -68,9 +68,11 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
public void Cleanup()
{
ExitScopeExe();
try
{
appDriver?.Kill();
appDriver?.WaitForExit(); // Optional: Wait for the process to exit
}
catch (Exception ex)
{
@@ -87,7 +89,53 @@ namespace Microsoft.PowerToys.UITest
{
var opts = new AppiumOptions();
opts.AddAdditionalCapability("app", appPath);
Console.WriteLine($"appPath: {appPath}");
this.Driver = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), opts);
// Set default timeout to 5 seconds
this.Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
}
/// <summary>
/// Exit a exe.
/// </summary>
/// <param name="path">The path to the application executable.</param>
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}");
}
}
}
/// <summary>
/// Exit now exe.
/// </summary>
public void ExitScopeExe()
{
ExitExe(sessionPath);
}
/// <summary>
/// Restarts now exe and takes control of it.
/// </summary>
public void RestartScopeExe()
{
ExitExe(sessionPath);
StartExe(locationPath + sessionPath);
}
public WindowsDriver<WindowsElement> GetRoot() => this.Root;

View File

@@ -6,6 +6,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Xml.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
@@ -31,11 +32,6 @@ namespace Microsoft.PowerToys.UITest
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver());
}
~UITestBase()
{
this.sessionHelper.Cleanup();
}
/// <summary>
/// Initializes the test.
/// </summary>
@@ -53,6 +49,15 @@ namespace Microsoft.PowerToys.UITest
}
}
/// <summary>
/// UnInitializes the test.
/// </summary>
[TestCleanup]
public void TestClean()
{
this.sessionHelper.Cleanup();
}
/// <summary>
/// Finds an element by selector.
/// Shortcut for this.Session.Find<T>(by, timeoutMS)
@@ -153,5 +158,24 @@ namespace Microsoft.PowerToys.UITest
{
return this.Session.FindAll<Element>(By.Name(name), timeoutMS);
}
/// <summary>
/// Restart scope exe.
/// </summary>
public void RestartScopeExe()
{
this.sessionHelper.RestartScopeExe();
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver());
return;
}
/// <summary>
/// Restart scope exe.
/// </summary>
public void ExitScopeExe()
{
this.sessionHelper.ExitScopeExe();
return;
}
}
}