// 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; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; using static Microsoft.PowerToys.UITest.WindowHelper; namespace Microsoft.PowerToys.UITest { /// /// Nested class for test initialization. /// public class SessionHelper { // Default session path is PowerToys settings dashboard private readonly string sessionPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.PowerToysSettings); private readonly string runnerPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.Runner); private string? locationPath; private static WindowsDriver? root; private WindowsDriver? Driver { get; set; } private static Process? appDriver; private Process? runner; private PowerToysModule scope; private string[]? commandLineArgs; /// /// Gets a value indicating whether to use installer paths for testing. /// private bool UseInstallerForTest { get; } [UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "")] public SessionHelper(PowerToysModule scope, string[]? commandLineArgs = null) { this.scope = scope; this.commandLineArgs = commandLineArgs; this.sessionPath = ModuleConfigData.Instance.GetModulePath(scope); UseInstallerForTest = EnvironmentConfig.UseInstallerForTest; this.locationPath = UseInstallerForTest ? string.Empty : Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); CheckWinAppDriverAndRoot(); } /// /// Initializes WinAppDriver And Root. /// public void CheckWinAppDriverAndRoot() { if (SessionHelper.root == null || SessionHelper.appDriver?.SessionId == null || SessionHelper.appDriver == null || SessionHelper.appDriver.HasExited) { this.StartWindowsAppDriverApp(); var desktopCapabilities = new AppiumOptions(); desktopCapabilities.AddAdditionalCapability("app", "Root"); SessionHelper.root = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), desktopCapabilities); } } /// /// Initializes the test environment. /// /// The PowerToys module to start. public SessionHelper Init() { this.ExitExe(this.locationPath + this.sessionPath); this.StartExe(this.locationPath + this.sessionPath, this.commandLineArgs); Assert.IsNotNull(this.Driver, $"Failed to initialize the test environment. Driver is null."); return this; } /// /// Cleans up the test environment. /// public void Cleanup() { ExitScopeExe(); } /// /// Exit a exe by Name. /// /// The path to the application executable. public void ExitExeByName(string processName) { Process[] processes = Process.GetProcessesByName(processName); 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}"); } } } /// /// Exit a exe. /// /// The path to the application executable. public void ExitExe(string appPath) { // Exit Exe string exeName = Path.GetFileNameWithoutExtension(appPath); ExitExeByName(exeName); } /// /// Starts a new exe and takes control of it. /// /// The path to the application executable. /// Optional command line arguments to pass to the application. public void StartExe(string appPath, string[]? args = null) { var opts = new AppiumOptions(); if (scope == PowerToysModule.PowerToysSettings) { TryLaunchPowerToysSettings(opts); } else if (scope == PowerToysModule.CommandPalette && UseInstallerForTest) { TryLaunchCommandPalette(opts); } else { opts.AddAdditionalCapability("app", appPath); if (args != null && args.Length > 0) { // Build command line arguments string string argsString = string.Join(" ", args.Select(arg => { // Quote arguments that contain spaces if (arg.Contains(' ')) { return $"\"{arg}\""; } return arg; })); opts.AddAdditionalCapability("appArguments", argsString); } } Driver = NewWindowsDriver(opts); } private void TryLaunchPowerToysSettings(AppiumOptions opts) { try { var runnerProcessInfo = new ProcessStartInfo { FileName = locationPath + runnerPath, Verb = "runas", Arguments = "--open-settings", }; ExitExe(runnerProcessInfo.FileName); runner = Process.Start(runnerProcessInfo); WaitForWindowAndSetCapability(opts, "PowerToys Settings", 5000, 5); // Exit CmdPal UI before launching new process if use installer for test ExitExeByName("Microsoft.CmdPal.UI"); } catch (Exception ex) { throw new InvalidOperationException($"Failed to launch PowerToys Settings: {ex.Message}", ex); } } private void TryLaunchCommandPalette(AppiumOptions opts) { try { // Exit any existing CmdPal UI process ExitExeByName("Microsoft.CmdPal.UI"); var processStartInfo = new ProcessStartInfo { FileName = "cmd.exe", Arguments = "/c start shell:appsFolder\\Microsoft.CommandPalette_8wekyb3d8bbwe!App", UseShellExecute = false, CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden, }; var process = Process.Start(processStartInfo); process?.WaitForExit(); WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10); } catch (Exception ex) { throw new InvalidOperationException($"Failed to launch Command Palette: {ex.Message}", ex); } } private void WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries) { for (int attempt = 1; attempt <= maxRetries; attempt++) { var window = ApiHelper.FindDesktopWindowHandler( [windowName, AdministratorPrefix + windowName]); if (window.Count > 0) { var hexHwnd = window[0].HWnd.ToString("x"); opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd); return; } if (attempt < maxRetries) { Thread.Sleep(delayMs); } else { throw new TimeoutException($"Failed to find {windowName} window after multiple attempts."); } } } /// /// Starts a new exe and takes control of it. /// /// The AppiumOptions for the application. private WindowsDriver NewWindowsDriver(AppiumOptions info) { // Create driver with retry var timeout = TimeSpan.FromMinutes(2); var retryInterval = TimeSpan.FromSeconds(5); DateTime startTime = DateTime.Now; while (true) { try { var res = new WindowsDriver(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), info); return res; } catch (Exception) { if (DateTime.Now - startTime > timeout) { throw; } Task.Delay(retryInterval).Wait(); CheckWinAppDriverAndRoot(); } } } /// /// Exit now exe. /// public void ExitScopeExe() { ExitExe(sessionPath); try { if (this.scope == PowerToysModule.PowerToysSettings) { runner?.Kill(); runner?.WaitForExit(); // Optional: Wait for the process to exit } } catch (Exception ex) { // Handle exceptions if needed Debug.WriteLine($"Exception during Cleanup: {ex.Message}"); } } /// /// Restarts now exe and takes control of it. /// public void RestartScopeExe() { ExitScopeExe(); StartExe(locationPath + sessionPath, this.commandLineArgs); } public WindowsDriver GetRoot() { return SessionHelper.root!; } public WindowsDriver GetDriver() { Assert.IsNotNull(this.Driver, $"Failed to get driver. Driver is null."); return this.Driver; } private void StartWindowsAppDriverApp() { var winAppDriverProcessInfo = new ProcessStartInfo { FileName = "C:\\Program Files (x86)\\Windows Application Driver\\WinAppDriver.exe", Verb = "runas", }; this.ExitExe(winAppDriverProcessInfo.FileName); SessionHelper.appDriver = Process.Start(winAppDriverProcessInfo); } } }