// 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 System.Collections.ObjectModel; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; using OpenQA.Selenium.Interactions; using static Microsoft.PowerToys.UITest.WindowHelper; namespace Microsoft.PowerToys.UITest { /// /// Provides interfaces for interacting with UI elements. /// public class Session { public WindowsDriver Root { get; set; } private WindowsDriver WindowsDriver { get; set; } private List windowHandlers = new List(); private Window? MainWindow { get; set; } /// /// Gets Main Window Handler /// public IntPtr MainWindowHandler { get; private set; } /// /// Gets Init Scope /// public PowerToysModule InitScope { get; private set; } /// /// 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. /// public bool? IsElevated { get; private set; } public Session(WindowsDriver pRoot, WindowsDriver 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); } } /// /// Cleans up the Session Exe. /// public void Cleanup() { windowHandlers.Clear(); } /// /// Finds an Element or its derived class by selector. /// /// The class of the element, should be Element or its derived class. /// The selector to find the element. /// The timeout in milliseconds (default is 5000). /// The found element. public T Find(By by, int timeoutMS = 5000, bool global = false) where T : Element, new() { Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}"); // leverage findAll to filter out mismatched elements var collection = this.FindAll(by, timeoutMS, global); Assert.IsTrue(collection.Count > 0, $"UI-Element({typeof(T).Name}) not found using selector: {by}"); return collection[0]; } /// /// Shortcut for this.Find(By.Name(name), timeoutMS) /// /// The class of the element, should be Element or its derived class. /// The name of the element. /// The timeout in milliseconds (default is 5000). /// The found element. public T Find(string name, int timeoutMS = 5000, bool global = false) where T : Element, new() { return this.Find(By.Name(name), timeoutMS, global); } /// /// Shortcut for this.Find(by, timeoutMS) /// /// The selector to find the element. /// The timeout in milliseconds (default is 5000). /// The found element. public Element Find(By by, int timeoutMS = 5000, bool global = false) { return this.Find(by, timeoutMS, global); } /// /// Shortcut for this.Find(By.Name(name), timeoutMS) /// /// The name of the element. /// The timeout in milliseconds (default is 5000). /// The found element. public Element Find(string name, int timeoutMS = 5000, bool global = false) { return this.Find(By.Name(name), timeoutMS, global); } /// /// Has only one Element or its derived class by selector. /// /// The class of the element, should be Element or its derived class. /// The name of the element. /// The timeout in milliseconds (default is 5000). /// True if only has one element, otherwise false. public bool HasOne(By by, int timeoutMS = 5000, bool global = false) where T : Element, new() { return this.FindAll(by, timeoutMS, global).Count == 1; } /// /// Shortcut for this.HasOne(by, timeoutMS) /// /// The name of the element. /// The timeout in milliseconds (default is 5000). /// True if only has one element, otherwise false. public bool HasOne(By by, int timeoutMS = 5000, bool global = false) { return this.HasOne(by, timeoutMS, global); } /// /// Shortcut for this.HasOne(By.Name(name), timeoutMS) /// /// The class of the element, should be Element or its derived class. /// The name of the element. /// The timeout in milliseconds (default is 5000). /// True if only has one element, otherwise false. public bool HasOne(string name, int timeoutMS = 5000, bool global = false) where T : Element, new() { return this.HasOne(By.Name(name), timeoutMS, global); } /// /// Shortcut for this.HasOne(name, timeoutMS) /// /// The name of the element. /// The timeout in milliseconds (default is 5000). /// True if only has one element, otherwise false. public bool HasOne(string name, int timeoutMS = 5000, bool global = false) { return this.HasOne(By.Name(name), timeoutMS, global); } /// /// Has one or more Element or its derived class by selector. /// /// The class of the element, should be Element or its derived class. /// The selector to find the element. /// The timeout in milliseconds (default is 5000). /// True if has one or more element, otherwise false. public bool Has(By by, int timeoutMS = 5000, bool global = false) where T : Element, new() { return this.FindAll(by, timeoutMS, global).Count >= 1; } /// /// Shortcut for this.Has(by, timeoutMS) /// /// The selector to find the element. /// The timeout in milliseconds (default is 5000). /// True if has one or more element, otherwise false. public bool Has(By by, int timeoutMS = 5000, bool global = false) { return this.Has(by, timeoutMS, global); } /// /// Shortcut for this.Has(By.Name(name), timeoutMS) /// /// The class of the element, should be Element or its derived class. /// The name of the element. /// The timeout in milliseconds (default is 5000). /// True if has one or more element, otherwise false. public bool Has(string name, int timeoutMS = 5000, bool global = false) where T : Element, new() { return this.Has(By.Name(name), timeoutMS, global); } /// /// Shortcut for this.Has(name, timeoutMS) /// /// The name of the element. /// The timeout in milliseconds (default is 5000). /// True if has one or more element, otherwise false. public bool Has(string name, int timeoutMS = 5000, bool global = false) { return this.Has(name, timeoutMS, global); } /// /// Finds all Element or its derived class by selector. /// /// The class of the elements, should be Element or its derived class. /// The selector to find the elements. /// The timeout in milliseconds (default is 5000). /// A read-only collection of the found elements. public ReadOnlyCollection FindAll(By by, int timeoutMS = 5000, bool global = false) where T : Element, new() { 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}"); var foundElements = FindHelper.FindAll( () => { if (by.GetIsAccessibilityId()) { var elements = driver.FindElementsByAccessibilityId(by.GetAccessibilityId()); return elements; } else { var elements = driver.FindElements(by.ToSeleniumBy()); return elements; } }, driver, timeoutMS); return foundElements ?? new ReadOnlyCollection([]); } /// /// Finds all elements by selector. /// Shortcut for this.FindAll(By.Name(name), timeoutMS) /// /// The class of the elements, should be Element or its derived class. /// The name to find the elements. /// The timeout in milliseconds (default is 5000). /// A read-only collection of the found elements. public ReadOnlyCollection FindAll(string name, int timeoutMS = 5000, bool global = false) where T : Element, new() { return this.FindAll(By.Name(name), timeoutMS, global); } /// /// Finds all elements by selector. /// Shortcut for this.FindAll(by, timeoutMS) /// /// The selector to find the elements. /// The timeout in milliseconds (default is 5000). /// A read-only collection of the found elements. public ReadOnlyCollection FindAll(By by, int timeoutMS = 5000, bool global = false) { return this.FindAll(by, timeoutMS, global); } /// /// Finds all elements by selector. /// Shortcut for this.FindAll(By.Name(name), timeoutMS) /// /// The name to find the elements. /// The timeout in milliseconds (default is 5000). /// A read-only collection of the found elements. public ReadOnlyCollection FindAll(string name, int timeoutMS = 5000, bool global = false) { return this.FindAll(By.Name(name), timeoutMS, global); } /// /// Close the main window. /// public void CloseMainWindow() { if (MainWindow != null) { MainWindow.Close(); MainWindow = null; } } /// /// Sends a combination of keys. /// /// The keys to send. public void SendKeys(params Key[] keys) { PerformAction(() => { KeyboardHelper.SendKeys(keys); }); } /// /// release the key (after the hold key and drag is completed.) /// /// The key release. public void PressKey(Key key) { PerformAction(() => { KeyboardHelper.PressKey(key); }); } /// /// press and hold the specified key. /// /// The key to press and hold . public void ReleaseKey(Key key) { PerformAction(() => { KeyboardHelper.ReleaseKey(key); }); } /// /// press and hold the specified key. /// /// The key to press and release . public void SendKey(Key key, int msPreAction = 500, int msPostAction = 500) { PerformAction( () => { KeyboardHelper.SendKey(key); }, msPreAction, msPostAction); } /// /// Sends a sequence of keys. /// /// An array of keys to send. public void SendKeySequence(params Key[] keys) { PerformAction(() => { foreach (var key in keys) { KeyboardHelper.SendKeys(key); } }); }         ///         /// Gets the current position of the mouse cursor as a tuple.         ///         /// A tuple containing the X and Y coordinates of the cursor. public Tuple GetMousePosition() { return MouseHelper.GetMousePosition(); } ///     /// Moves the mouse cursor to the specified screen coordinates.     ///     /// The new x-coordinate of the cursor.     /// The new y-coordinate of the cursor. { MouseHelper.MoveMouseTo(x, y); }, msPreAction, msPostAction); } /// /// Performs a mouse action based on the specified action type. /// /// The mouse action to perform. /// Pre-action delay in milliseconds. /// Post-action delay in milliseconds. 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); } /// /// Attaches to an existing PowerToys module. /// /// The PowerToys module to attach to. /// The window size to set. Default is no change to window size /// The attached session. public Session Attach(PowerToysModule module, WindowSize size = WindowSize.UnSpecified) { string windowName = ModuleConfigData.Instance.GetModuleWindowName(module); return this.Attach(windowName, size); } /// /// Attaches to an existing exe by string window name. /// The session should be attached when a new app is started. /// /// The window name to attach to. /// The window size to set. Default is no change to window size /// The attached session. public Session Attach(string windowName, WindowSize size = WindowSize.UnSpecified) { this.IsElevated = null; this.MainWindowHandler = IntPtr.Zero; if (this.Root != null) { // 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; 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"); var appCapabilities = new AppiumOptions(); appCapabilities.AddAdditionalCapability("appTopLevelWindow", hexWindowHandle); appCapabilities.AddAdditionalCapability("deviceName", "WindowsPC"); this.WindowsDriver = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), appCapabilities); this.windowHandlers.Add(this.MainWindowHandler); if (size != WindowSize.UnSpecified) { WindowHelper.SetWindowSize(this.MainWindowHandler, size); } // Set MainWindow MainWindow = Find(matchingWindows[0].Title); } else { Assert.IsNotNull(this.Root, $"Failed to attach to the window '{windowName}'. Root driver is null"); } Task.Delay(3000).Wait(); return this; } /// /// Sets the main window size. /// /// WindowSize enum 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); } /// /// Gets the main window center coordinates. /// /// (x, y) public (int CenterX, int CenterY) GetMainWindowCenter() { return WindowHelper.GetWindowCenter(this.MainWindowHandler); } /// /// Gets the main window center coordinates. /// /// (int Left, int Top, int Right, int Bottom) public (int Left, int Top, int Right, int Bottom) GetMainWindowRect() { return WindowHelper.GetWindowRect(this.MainWindowHandler); } /// /// Launches the specified executable with optional arguments and simulates a delay before and after execution. /// /// The full path to the executable to launch. /// Optional command-line arguments to pass to the executable. /// The number of milliseconds to wait before launching the executable. Default is 0 ms. /// The number of milliseconds to wait after launching the executable. Default is 2000 ms. 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); } /// /// Terminates all running processes that match the specified process name. /// Waits for each process to exit after sending the kill signal. /// /// The name of the process to terminate (without extension, e.g., "notepad"). public void KillAllProcessesByName(string processName) { foreach (var process in Process.GetProcessesByName(processName)) { process.Kill(); process.WaitForExit(); } } /// /// Simulates a manual operation on the element. /// /// The action to perform on the element. /// The number of milliseconds to wait before the action. Default value is 500 ms /// The number of milliseconds to wait after the action. Default value is 500 ms protected void PerformAction(Action> 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(); } } /// /// Simulates a manual operation on the element. /// /// The action to perform on the element. /// The number of milliseconds to wait before the action. Default value is 500 ms /// The number of milliseconds to wait after the action. Default value is 500 ms 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(); } } } }