2025-06-17 17:56:48 +08:00
|
|
|
|
// Copyright (c) Microsoft Corporation
|
2025-02-20 13:25:20 +08:00
|
|
|
|
// 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.Collections.ObjectModel;
|
2025-06-17 17:56:48 +08:00
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
|
using System.Runtime.CompilerServices;
|
2025-02-20 13:25:20 +08:00
|
|
|
|
using System.Runtime.InteropServices;
|
2025-06-17 17:56:48 +08:00
|
|
|
|
using System.Text;
|
2025-02-20 13:25:20 +08:00
|
|
|
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
|
|
|
|
using OpenQA.Selenium.Appium;
|
|
|
|
|
|
using OpenQA.Selenium.Appium.Windows;
|
2025-03-14 17:04:23 +08:00
|
|
|
|
using OpenQA.Selenium.Interactions;
|
2025-06-17 17:56:48 +08:00
|
|
|
|
using static Microsoft.PowerToys.UITest.WindowHelper;
|
2025-02-20 13:25:20 +08:00
|
|
|
|
|
|
|
|
|
|
namespace Microsoft.PowerToys.UITest
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Provides interfaces for interacting with UI elements.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class Session
|
|
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
public WindowsDriver<WindowsElement> Root { get; set; }
|
2025-02-20 13:25:20 +08:00
|
|
|
|
|
|
|
|
|
|
private WindowsDriver<WindowsElement> WindowsDriver { get; set; }
|
|
|
|
|
|
|
2025-06-17 17:56:48 +08:00
|
|
|
|
private List<IntPtr> windowHandlers = new List<IntPtr>();
|
2025-02-20 13:25:20 +08:00
|
|
|
|
|
2025-06-17 17:56:48 +08:00
|
|
|
|
private Window? MainWindow { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets Main Window Handler
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public IntPtr MainWindowHandler { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets Init Scope
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public PowerToysModule InitScope { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets the RunAsAdmin flag.
|
|
|
|
|
|
/// If true, the session is running as admin.
|
|
|
|
|
|
/// If false, the session is not running as admin.
|
|
|
|
|
|
/// If null, no information is available.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public bool? IsElevated { get; private set; }
|
|
|
|
|
|
|
|
|
|
|
|
public Session(WindowsDriver<WindowsElement> pRoot, WindowsDriver<WindowsElement> pDriver, PowerToysModule scope, WindowSize size)
|
|
|
|
|
|
{
|
|
|
|
|
|
this.MainWindowHandler = IntPtr.Zero;
|
|
|
|
|
|
this.Root = pRoot;
|
|
|
|
|
|
this.WindowsDriver = pDriver;
|
|
|
|
|
|
this.InitScope = scope;
|
|
|
|
|
|
|
|
|
|
|
|
if (size != WindowSize.UnSpecified)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Attach to the scope & reset MainWindowHandler
|
|
|
|
|
|
this.Attach(scope, size);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Cleans up the Session Exe.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void Cleanup()
|
2025-02-20 13:25:20 +08:00
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
windowHandlers.Clear();
|
2025-02-20 13:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// Finds an Element or its derived class by selector.
|
2025-02-20 13:25:20 +08:00
|
|
|
|
/// </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>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
2025-02-20 13:25:20 +08:00
|
|
|
|
/// <returns>The found element.</returns>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
public T Find<T>(By by, int timeoutMS = 5000, bool global = false)
|
2025-02-20 13:25:20 +08:00
|
|
|
|
where T : Element, new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
|
|
|
|
|
|
|
2025-02-28 03:08:08 -08:00
|
|
|
|
// leverage findAll to filter out mismatched elements
|
2025-06-17 17:56:48 +08:00
|
|
|
|
var collection = this.FindAll<T>(by, timeoutMS, global);
|
2025-02-28 03:08:08 -08:00
|
|
|
|
|
2025-06-17 17:56:48 +08:00
|
|
|
|
Assert.IsTrue(collection.Count > 0, $"UI-Element({typeof(T).Name}) not found using selector: {by}");
|
2025-02-28 03:08:08 -08:00
|
|
|
|
|
|
|
|
|
|
return collection[0];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Shortcut for this.Find<T>(By.Name(name), timeoutMS)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
|
|
|
|
|
|
/// <param name="name">The name of the element.</param>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
2025-02-28 03:08:08 -08:00
|
|
|
|
/// <returns>The found element.</returns>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
public T Find<T>(string name, int timeoutMS = 5000, bool global = false)
|
2025-02-28 03:08:08 -08:00
|
|
|
|
where T : Element, new()
|
|
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
return this.Find<T>(By.Name(name), timeoutMS, global);
|
2025-02-28 03:08:08 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Shortcut for this.Find<Element>(by, timeoutMS)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="by">The selector to find the element.</param>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
2025-02-28 03:08:08 -08:00
|
|
|
|
/// <returns>The found element.</returns>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
public Element Find(By by, int timeoutMS = 5000, bool global = false)
|
2025-02-28 03:08:08 -08:00
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
return this.Find<Element>(by, timeoutMS, global);
|
2025-02-28 03:08:08 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Shortcut for this.Find<Element>(By.Name(name), timeoutMS)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="name">The name of the element.</param>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
2025-02-28 03:08:08 -08:00
|
|
|
|
/// <returns>The found element.</returns>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
public Element Find(string name, int timeoutMS = 5000, bool global = false)
|
2025-02-28 03:08:08 -08:00
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
return this.Find<Element>(By.Name(name), timeoutMS, global);
|
2025-02-20 13:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// Has only one Element or its derived class by selector.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
|
|
|
|
|
|
/// <param name="by">The name of the element.</param>
|
|
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
|
|
|
|
|
/// <returns>True if only has one element, otherwise false.</returns>
|
|
|
|
|
|
public bool HasOne<T>(By by, int timeoutMS = 5000, bool global = false)
|
|
|
|
|
|
where T : Element, new()
|
|
|
|
|
|
{
|
|
|
|
|
|
return this.FindAll<T>(by, timeoutMS, global).Count == 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Shortcut for this.HasOne<Element>(by, timeoutMS)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="by">The name of the element.</param>
|
|
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
|
|
|
|
|
/// <returns>True if only has one element, otherwise false.</returns>
|
|
|
|
|
|
public bool HasOne(By by, int timeoutMS = 5000, bool global = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
return this.HasOne<Element>(by, timeoutMS, global);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Shortcut for this.HasOne<T>(By.Name(name), timeoutMS)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
|
|
|
|
|
|
/// <param name="name">The name of the element.</param>
|
|
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
|
|
|
|
|
/// <returns>True if only has one element, otherwise false.</returns>
|
|
|
|
|
|
public bool HasOne<T>(string name, int timeoutMS = 5000, bool global = false)
|
|
|
|
|
|
where T : Element, new()
|
|
|
|
|
|
{
|
|
|
|
|
|
return this.HasOne<T>(By.Name(name), timeoutMS, global);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Shortcut for this.HasOne<Element>(name, timeoutMS)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="name">The name of the element.</param>
|
|
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
|
|
|
|
|
/// <returns>True if only has one element, otherwise false.</returns>
|
|
|
|
|
|
public bool HasOne(string name, int timeoutMS = 5000, bool global = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
return this.HasOne<Element>(By.Name(name), timeoutMS, global);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Has one or more Element or its derived class by selector.
|
|
|
|
|
|
/// </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 5000).</param>
|
|
|
|
|
|
/// <returns>True if has one or more element, otherwise false.</returns>
|
|
|
|
|
|
public bool Has<T>(By by, int timeoutMS = 5000, bool global = false)
|
|
|
|
|
|
where T : Element, new()
|
|
|
|
|
|
{
|
|
|
|
|
|
return this.FindAll<T>(by, timeoutMS, global).Count >= 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Shortcut for this.Has<Element>(by, timeoutMS)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="by">The selector to find the element.</param>
|
|
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
|
|
|
|
|
/// <returns>True if has one or more element, otherwise false.</returns>
|
|
|
|
|
|
public bool Has(By by, int timeoutMS = 5000, bool global = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
return this.Has<Element>(by, timeoutMS, global);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Shortcut for this.Has<T>(By.Name(name), timeoutMS)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
|
|
|
|
|
|
/// <param name="name">The name of the element.</param>
|
|
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
|
|
|
|
|
/// <returns>True if has one or more element, otherwise false.</returns>
|
|
|
|
|
|
public bool Has<T>(string name, int timeoutMS = 5000, bool global = false)
|
|
|
|
|
|
where T : Element, new()
|
|
|
|
|
|
{
|
|
|
|
|
|
return this.Has<T>(By.Name(name), timeoutMS, global);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Shortcut for this.Has<Element>(name, timeoutMS)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="name">The name of the element.</param>
|
|
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
|
|
|
|
|
/// <returns>True if has one or more element, otherwise false.</returns>
|
|
|
|
|
|
public bool Has(string name, int timeoutMS = 5000, bool global = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
return this.Has<Element>(name, timeoutMS, global);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Finds all Element or its derived class by selector.
|
2025-02-20 13:25:20 +08:00
|
|
|
|
/// </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>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
2025-02-20 13:25:20 +08:00
|
|
|
|
/// <returns>A read-only collection of the found elements.</returns>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
public ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 5000, bool global = false)
|
2025-02-20 13:25:20 +08:00
|
|
|
|
where T : Element, new()
|
|
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
var driver = global ? this.Root : this.WindowsDriver;
|
|
|
|
|
|
Assert.IsNotNull(driver, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
|
2025-02-24 02:05:55 -08:00
|
|
|
|
var foundElements = FindHelper.FindAll<T, WindowsElement>(
|
2025-02-20 13:25:20 +08:00
|
|
|
|
() =>
|
|
|
|
|
|
{
|
2025-03-14 17:04:23 +08:00
|
|
|
|
if (by.GetIsAccessibilityId())
|
|
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
var elements = driver.FindElementsByAccessibilityId(by.GetAccessibilityId());
|
2025-03-14 17:04:23 +08:00
|
|
|
|
return elements;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
var elements = driver.FindElements(by.ToSeleniumBy());
|
2025-03-14 17:04:23 +08:00
|
|
|
|
return elements;
|
|
|
|
|
|
}
|
2025-02-20 13:25:20 +08:00
|
|
|
|
},
|
2025-06-17 17:56:48 +08:00
|
|
|
|
driver,
|
2025-02-20 13:25:20 +08:00
|
|
|
|
timeoutMS);
|
|
|
|
|
|
|
2025-02-28 03:08:08 -08:00
|
|
|
|
return foundElements ?? new ReadOnlyCollection<T>([]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Finds all elements by selector.
|
|
|
|
|
|
/// Shortcut for this.FindAll<T>(By.Name(name), timeoutMS)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <typeparam name="T">The class of the elements, should be Element or its derived class.</typeparam>
|
|
|
|
|
|
/// <param name="name">The name to find the elements.</param>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
2025-02-28 03:08:08 -08:00
|
|
|
|
/// <returns>A read-only collection of the found elements.</returns>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
public ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 5000, bool global = false)
|
2025-02-28 03:08:08 -08:00
|
|
|
|
where T : Element, new()
|
|
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
return this.FindAll<T>(By.Name(name), timeoutMS, global);
|
2025-02-28 03:08:08 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Finds all elements by selector.
|
|
|
|
|
|
/// Shortcut for this.FindAll<Element>(by, timeoutMS)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="by">The selector to find the elements.</param>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
2025-02-28 03:08:08 -08:00
|
|
|
|
/// <returns>A read-only collection of the found elements.</returns>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
public ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 5000, bool global = false)
|
2025-02-28 03:08:08 -08:00
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
return this.FindAll<Element>(by, timeoutMS, global);
|
2025-02-28 03:08:08 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Finds all elements by selector.
|
|
|
|
|
|
/// Shortcut for this.FindAll<Element>(By.Name(name), timeoutMS)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="name">The name to find the elements.</param>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
2025-02-28 03:08:08 -08:00
|
|
|
|
/// <returns>A read-only collection of the found elements.</returns>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
public ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 5000, bool global = false)
|
2025-02-28 03:08:08 -08:00
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
return this.FindAll<Element>(By.Name(name), timeoutMS, global);
|
2025-02-20 13:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-14 17:04:23 +08:00
|
|
|
|
/// <summary>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// Close the main window.
|
2025-03-14 17:04:23 +08:00
|
|
|
|
/// </summary>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
public void CloseMainWindow()
|
2025-03-14 17:04:23 +08:00
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
if (MainWindow != null)
|
2025-03-14 17:04:23 +08:00
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
MainWindow.Close();
|
|
|
|
|
|
MainWindow = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Sends a combination of keys.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="keys">The keys to send.</param>
|
|
|
|
|
|
public void SendKeys(params Key[] keys)
|
|
|
|
|
|
{
|
|
|
|
|
|
PerformAction(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
KeyboardHelper.SendKeys(keys);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// release the key (after the hold key and drag is completed.)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="key">The key release.</param>
|
|
|
|
|
|
public void PressKey(Key key)
|
|
|
|
|
|
{
|
|
|
|
|
|
PerformAction(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
KeyboardHelper.PressKey(key);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// press and hold the specified key.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="key">The key to press and hold .</param>
|
|
|
|
|
|
public void ReleaseKey(Key key)
|
|
|
|
|
|
{
|
|
|
|
|
|
PerformAction(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
KeyboardHelper.ReleaseKey(key);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// press and hold the specified key.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="key">The key to press and release .</param>
|
|
|
|
|
|
public void SendKey(Key key, int msPreAction = 500, int msPostAction = 500)
|
|
|
|
|
|
{
|
|
|
|
|
|
PerformAction(
|
|
|
|
|
|
() =>
|
2025-03-14 17:04:23 +08:00
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
KeyboardHelper.SendKey(key);
|
|
|
|
|
|
},
|
|
|
|
|
|
msPreAction,
|
|
|
|
|
|
msPostAction);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Sends a sequence of keys.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="keys">An array of keys to send.</param>
|
|
|
|
|
|
public void SendKeySequence(params Key[] keys)
|
|
|
|
|
|
{
|
|
|
|
|
|
PerformAction(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var key in keys)
|
2025-03-14 17:04:23 +08:00
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
KeyboardHelper.SendKeys(key);
|
2025-03-14 17:04:23 +08:00
|
|
|
|
}
|
2025-06-17 17:56:48 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets the current position of the mouse cursor as a tuple.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns>A tuple containing the X and Y coordinates of the cursor.</returns>
|
|
|
|
|
|
public Tuple<int, int> GetMousePosition()
|
|
|
|
|
|
{
|
|
|
|
|
|
return MouseHelper.GetMousePosition();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Moves the mouse cursor to the specified screen coordinates.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="x">The new x-coordinate of the cursor.</param>
|
|
|
|
|
|
/// <param name="y">The new y-coordinate of the cursor.</param
|
|
|
|
|
|
public void MoveMouseTo(int x, int y, int msPreAction = 500, int msPostAction = 500)
|
|
|
|
|
|
{
|
|
|
|
|
|
PerformAction(
|
|
|
|
|
|
() =>
|
2025-03-14 17:04:23 +08:00
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
MouseHelper.MoveMouseTo(x, y);
|
|
|
|
|
|
},
|
|
|
|
|
|
msPreAction,
|
|
|
|
|
|
msPostAction);
|
|
|
|
|
|
}
|
2025-03-14 17:04:23 +08:00
|
|
|
|
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Performs a mouse action based on the specified action type.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="action">The mouse action to perform.</param>
|
|
|
|
|
|
/// <param name="msPreAction">Pre-action delay in milliseconds.</param>
|
|
|
|
|
|
/// <param name="msPostAction">Post-action delay in milliseconds.</param>
|
|
|
|
|
|
public void PerformMouseAction(MouseActionType action, int msPreAction = 500, int msPostAction = 500)
|
|
|
|
|
|
{
|
|
|
|
|
|
PerformAction(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (action)
|
|
|
|
|
|
{
|
|
|
|
|
|
case MouseActionType.LeftClick:
|
|
|
|
|
|
MouseHelper.LeftClick();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case MouseActionType.RightClick:
|
|
|
|
|
|
MouseHelper.RightClick();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case MouseActionType.MiddleClick:
|
|
|
|
|
|
MouseHelper.MiddleClick();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case MouseActionType.LeftDoubleClick:
|
|
|
|
|
|
MouseHelper.LeftDoubleClick();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case MouseActionType.RightDoubleClick:
|
|
|
|
|
|
MouseHelper.RightDoubleClick();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case MouseActionType.LeftDown:
|
|
|
|
|
|
MouseHelper.LeftDown();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case MouseActionType.LeftUp:
|
|
|
|
|
|
MouseHelper.LeftUp();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case MouseActionType.RightDown:
|
|
|
|
|
|
MouseHelper.RightDown();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case MouseActionType.RightUp:
|
|
|
|
|
|
MouseHelper.RightUp();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case MouseActionType.MiddleDown:
|
|
|
|
|
|
MouseHelper.MiddleDown();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case MouseActionType.MiddleUp:
|
|
|
|
|
|
MouseHelper.MiddleUp();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case MouseActionType.ScrollUp:
|
|
|
|
|
|
MouseHelper.ScrollUp();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case MouseActionType.ScrollDown:
|
|
|
|
|
|
MouseHelper.ScrollDown();
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
throw new ArgumentException("Unsupported mouse action.", nameof(action));
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
msPreAction,
|
|
|
|
|
|
msPostAction);
|
2025-03-14 17:04:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-20 13:25:20 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Attaches to an existing PowerToys module.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="module">The PowerToys module to attach to.</param>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// <param name="size">The window size to set. Default is no change to window size</param>
|
2025-02-20 13:25:20 +08:00
|
|
|
|
/// <returns>The attached session.</returns>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
public Session Attach(PowerToysModule module, WindowSize size = WindowSize.UnSpecified)
|
2025-02-20 13:25:20 +08:00
|
|
|
|
{
|
|
|
|
|
|
string windowName = ModuleConfigData.Instance.GetModuleWindowName(module);
|
2025-06-17 17:56:48 +08:00
|
|
|
|
return this.Attach(windowName, size);
|
2025-02-20 13:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Attaches to an existing exe by string window name.
|
|
|
|
|
|
/// The session should be attached when a new app is started.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="windowName">The window name to attach to.</param>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// <param name="size">The window size to set. Default is no change to window size</param>
|
2025-02-20 13:25:20 +08:00
|
|
|
|
/// <returns>The attached session.</returns>
|
2025-06-17 17:56:48 +08:00
|
|
|
|
public Session Attach(string windowName, WindowSize size = WindowSize.UnSpecified)
|
2025-02-20 13:25:20 +08:00
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
this.IsElevated = null;
|
|
|
|
|
|
this.MainWindowHandler = IntPtr.Zero;
|
|
|
|
|
|
|
2025-02-20 13:25:20 +08:00
|
|
|
|
if (this.Root != null)
|
|
|
|
|
|
{
|
2025-06-17 17:56:48 +08:00
|
|
|
|
// search window handler by window title (admin and non-admin titles)
|
|
|
|
|
|
var timeout = TimeSpan.FromMinutes(2);
|
|
|
|
|
|
var retryInterval = TimeSpan.FromSeconds(5);
|
|
|
|
|
|
DateTime startTime = DateTime.Now;
|
2025-02-20 13:25:20 +08:00
|
|
|
|
|
2025-06-17 17:56:48 +08:00
|
|
|
|
List<(IntPtr HWnd, string Title)>? matchingWindows = null;
|
|
|
|
|
|
|
|
|
|
|
|
while (DateTime.Now - startTime < timeout)
|
|
|
|
|
|
{
|
|
|
|
|
|
matchingWindows = WindowHelper.ApiHelper.FindDesktopWindowHandler(
|
|
|
|
|
|
new[] { windowName, WindowHelper.AdministratorPrefix + windowName });
|
|
|
|
|
|
|
|
|
|
|
|
if (matchingWindows.Count > 0 && matchingWindows[0].HWnd != IntPtr.Zero)
|
|
|
|
|
|
{
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Task.Delay(retryInterval).Wait();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (matchingWindows == null || matchingWindows.Count == 0 || matchingWindows[0].HWnd == IntPtr.Zero)
|
|
|
|
|
|
{
|
|
|
|
|
|
Assert.Fail($"Failed to attach. Window '{windowName}' not found after {timeout.TotalSeconds} seconds.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// pick one from matching windows
|
|
|
|
|
|
this.MainWindowHandler = matchingWindows[0].HWnd;
|
|
|
|
|
|
this.IsElevated = matchingWindows[0].Title.StartsWith(WindowHelper.AdministratorPrefix);
|
|
|
|
|
|
|
|
|
|
|
|
ApiHelper.SetForegroundWindow(this.MainWindowHandler);
|
|
|
|
|
|
|
|
|
|
|
|
var hexWindowHandle = this.MainWindowHandler.ToInt64().ToString("x");
|
2025-02-24 02:05:55 -08:00
|
|
|
|
|
2025-06-17 17:56:48 +08:00
|
|
|
|
var appCapabilities = new AppiumOptions();
|
2025-02-20 13:25:20 +08:00
|
|
|
|
appCapabilities.AddAdditionalCapability("appTopLevelWindow", hexWindowHandle);
|
|
|
|
|
|
appCapabilities.AddAdditionalCapability("deviceName", "WindowsPC");
|
|
|
|
|
|
this.WindowsDriver = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), appCapabilities);
|
|
|
|
|
|
|
2025-06-17 17:56:48 +08:00
|
|
|
|
this.windowHandlers.Add(this.MainWindowHandler);
|
|
|
|
|
|
|
|
|
|
|
|
if (size != WindowSize.UnSpecified)
|
|
|
|
|
|
{
|
|
|
|
|
|
WindowHelper.SetWindowSize(this.MainWindowHandler, size);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Set MainWindow
|
|
|
|
|
|
MainWindow = Find<Window>(matchingWindows[0].Title);
|
2025-02-20 13:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Assert.IsNotNull(this.Root, $"Failed to attach to the window '{windowName}'. Root driver is null");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-17 17:56:48 +08:00
|
|
|
|
Task.Delay(3000).Wait();
|
2025-02-20 13:25:20 +08:00
|
|
|
|
return this;
|
|
|
|
|
|
}
|
2025-03-14 17:04:23 +08:00
|
|
|
|
|
2025-06-17 17:56:48 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Sets the main window size.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="size">WindowSize enum</param>
|
|
|
|
|
|
public void SetMainWindowSize(WindowSize size)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (this.MainWindowHandler == IntPtr.Zero)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Attach to the scope & reset MainWindowHandler
|
|
|
|
|
|
this.Attach(this.InitScope);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
WindowHelper.SetWindowSize(this.MainWindowHandler, size);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets the main window center coordinates.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns>(x, y)</returns>
|
|
|
|
|
|
public (int CenterX, int CenterY) GetMainWindowCenter()
|
|
|
|
|
|
{
|
|
|
|
|
|
return WindowHelper.GetWindowCenter(this.MainWindowHandler);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets the main window center coordinates.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns>(int Left, int Top, int Right, int Bottom)</returns>
|
|
|
|
|
|
public (int Left, int Top, int Right, int Bottom) GetMainWindowRect()
|
|
|
|
|
|
{
|
|
|
|
|
|
return WindowHelper.GetWindowRect(this.MainWindowHandler);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Launches the specified executable with optional arguments and simulates a delay before and after execution.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="executablePath">The full path to the executable to launch.</param>
|
|
|
|
|
|
/// <param name="arguments">Optional command-line arguments to pass to the executable.</param>
|
|
|
|
|
|
/// <param name="msPreAction">The number of milliseconds to wait before launching the executable. Default is 0 ms.</param>
|
|
|
|
|
|
/// <param name="msPostAction">The number of milliseconds to wait after launching the executable. Default is 2000 ms.</param>
|
|
|
|
|
|
public void StartExe(string executablePath, string arguments = "", int msPreAction = 0, int msPostAction = 2000)
|
|
|
|
|
|
{
|
|
|
|
|
|
PerformAction(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
StartExeInternal(executablePath, arguments);
|
|
|
|
|
|
},
|
|
|
|
|
|
msPreAction,
|
|
|
|
|
|
msPostAction);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void StartExeInternal(string executablePath, string arguments = "")
|
|
|
|
|
|
{
|
|
|
|
|
|
var processInfo = new ProcessStartInfo
|
|
|
|
|
|
{
|
|
|
|
|
|
FileName = executablePath,
|
|
|
|
|
|
Arguments = arguments,
|
|
|
|
|
|
UseShellExecute = true,
|
|
|
|
|
|
};
|
|
|
|
|
|
Process.Start(processInfo);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Terminates all running processes that match the specified process name.
|
|
|
|
|
|
/// Waits for each process to exit after sending the kill signal.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="processName">The name of the process to terminate (without extension, e.g., "notepad").</param>
|
|
|
|
|
|
public void KillAllProcessesByName(string processName)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var process in Process.GetProcessesByName(processName))
|
|
|
|
|
|
{
|
|
|
|
|
|
process.Kill();
|
|
|
|
|
|
process.WaitForExit();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-14 17:04:23 +08:00
|
|
|
|
/// <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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-17 17:56:48 +08:00
|
|
|
|
|
|
|
|
|
|
/// <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 action, int msPreAction = 500, int msPostAction = 500)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (msPreAction > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
Task.Delay(msPreAction).Wait();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
action();
|
|
|
|
|
|
|
|
|
|
|
|
if (msPostAction > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
Task.Delay(msPostAction).Wait();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-02-20 13:25:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|