// 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.Runtime.InteropServices; 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 { /// /// Provides interfaces for interacting with UI elements. /// public class Session { private WindowsDriver Root { get; set; } private WindowsDriver WindowsDriver { get; set; } [DllImport("user32.dll")] private static extern bool SetForegroundWindow(nint hWnd); public Session(WindowsDriver root, WindowsDriver windowsDriver) { this.Root = root; this.WindowsDriver = windowsDriver; } /// /// Finds an element 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 3000). /// The found element. public T Find(By by, int timeoutMS = 3000) 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); Assert.IsTrue(collection.Count > 0, $"Element 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 3000). /// The found element. public T Find(string name, int timeoutMS = 3000) where T : Element, new() { return this.Find(By.Name(name), timeoutMS); } /// /// Shortcut for this.Find(by, timeoutMS) /// /// The selector to find the element. /// The timeout in milliseconds (default is 3000). /// The found element. public Element Find(By by, int timeoutMS = 3000) { return this.Find(by, timeoutMS); } /// /// Shortcut for this.Find(By.Name(name), timeoutMS) /// /// The name of the element. /// The timeout in milliseconds (default is 3000). /// The found element. public Element Find(string name, int timeoutMS = 3000) { return this.Find(By.Name(name), timeoutMS); } /// /// Finds all elements 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 3000). /// A read-only collection of the found elements. public ReadOnlyCollection FindAll(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 = FindHelper.FindAll( () => { 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); 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 3000). /// A read-only collection of the found elements. public ReadOnlyCollection FindAll(string name, int timeoutMS = 3000) where T : Element, new() { return this.FindAll(By.Name(name), timeoutMS); } /// /// Finds all elements by selector. /// Shortcut for this.FindAll(by, timeoutMS) /// /// The selector to find the elements. /// The timeout in milliseconds (default is 3000). /// A read-only collection of the found elements. public ReadOnlyCollection FindAll(By by, int timeoutMS = 3000) { return this.FindAll(by, timeoutMS); } /// /// 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 3000). /// A read-only collection of the found elements. public ReadOnlyCollection FindAll(string name, int timeoutMS = 3000) { return this.FindAll(By.Name(name), timeoutMS); } /// /// Keyboard Action key. /// /// The Keys1 to click. /// The Keys2 to click. /// The Keys3 to click. /// The Keys4 to click. 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(); }); } /// /// Attaches to an existing PowerToys module. /// /// The PowerToys module to attach to. /// The attached session. public Session Attach(PowerToysModule module) { string windowName = ModuleConfigData.Instance.GetModuleWindowName(module); return this.Attach(windowName); } /// /// 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 attached session. public Session Attach(string windowName) { if (this.Root != null) { var window = this.Root.FindElementByName(windowName); Assert.IsNotNull(window, $"Failed to attach. Window '{windowName}' not found"); var windowHandle = new nint(int.Parse(window.GetAttribute("NativeWindowHandle"))); SetForegroundWindow(windowHandle); var hexWindowHandle = windowHandle.ToString("x"); var appCapabilities = new AppiumOptions(); appCapabilities.AddAdditionalCapability("appTopLevelWindow", hexWindowHandle); appCapabilities.AddAdditionalCapability("deviceName", "WindowsPC"); this.WindowsDriver = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), appCapabilities); Assert.IsNotNull(this.WindowsDriver, "Attach WindowsDriver is null"); // Set implicit timeout to make element search retry every 500 ms this.WindowsDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3); } else { Assert.IsNotNull(this.Root, $"Failed to attach to the window '{windowName}'. Root driver is null"); } return this; } /// /// 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(); } } } }