Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions src/common/UITestAutomation/Element/By.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -31,13 +54,27 @@ public override string ToString()
/// <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>
/// <param name="id">The ID attribute to search for.</param>
/// <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>
Expand Down Expand Up @@ -70,6 +107,6 @@ public override string ToString()
/// 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!;
}
}
73 changes: 71 additions & 2 deletions src/common/UITestAutomation/Element/Element.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,6 +68,14 @@ public bool Selected
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>
Expand Down Expand Up @@ -138,6 +151,54 @@ public virtual void DoubleClick()
});
}

/// <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>
Expand Down Expand Up @@ -222,8 +283,16 @@ public ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 3000)
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);
Expand Down
13 changes: 13 additions & 0 deletions src/common/UITestAutomation/FindHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
69 changes: 67 additions & 2 deletions src/common/UITestAutomation/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;

namespace Microsoft.PowerToys.UITest
{
Expand Down Expand Up @@ -98,8 +99,16 @@ public ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 3000)
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);
Expand Down Expand Up @@ -145,6 +154,39 @@ public ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 3000)
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>
Expand Down Expand Up @@ -189,5 +231,28 @@ public Session Attach(string windowName)

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();
}
}
}
}
53 changes: 50 additions & 3 deletions src/common/UITestAutomation/SessionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,15 +20,19 @@ internal class SessionHelper
// 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
{
Expand All @@ -49,11 +54,9 @@ public SessionHelper(PowerToysModule scope)
/// 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.");

Expand All @@ -68,6 +71,7 @@ public SessionHelper Init()
/// </summary>
public void Cleanup()
{
ExitScopeExe();
try
{
appDriver?.Kill();
Expand All @@ -87,9 +91,52 @@ public void StartExe(string appPath)
{
var opts = new AppiumOptions();
opts.AddAdditionalCapability("app", appPath);
Console.WriteLine($"appPath: {appPath}");
this.Driver = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), opts);
}

/// <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;

public WindowsDriver<WindowsElement> GetDriver()
Expand Down
Loading
Loading