// 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();
}
}
}
}