import { normalize } from 'node:path'; import { Element, Rect } from '@appium/types'; import { NovaWindowsDriver } from '../driver'; import { AutomationElement, FoundAutomationElement, PSInt32, PSString, Property, PropertyCondition, TreeScope, TrueCondition, pwsh$, pwsh, } from '../powershell'; import { sleep } from '../util'; import { errors, W3C_ELEMENT_KEY } from '@appium/base-driver'; import { // getWindowAllHandlesForProcessIds, keyDown, keyUp, trySetForegroundWindow, } from '../winapi/user32'; import { Key } from '../enums'; const GET_PAGE_SOURCE_COMMAND = pwsh$ /* ps1 */ ` $el = ${0} if ($el -eq $null) { $dummy = [xml]'' return $dummy.OuterXml } Get-PageSource $el | ForEach-Object { $_.OuterXml } `; const GET_SCREENSHOT_COMMAND = pwsh /* ps1 */ ` if ($rootElement -eq $null) { $bitmap = New-Object Drawing.Bitmap 1,1 $stream = New-Object IO.MemoryStream $bitmap.Save($stream, [Drawing.Imaging.ImageFormat]::Png) $bitmap.Dispose() return [Convert]::ToBase64String($stream.ToArray()) } $rect = $rootElement.Current.BoundingRectangle $bitmap = New-Object Drawing.Bitmap([int32]$rect.Width, [int32]$rect.Height) $graphics = [Drawing.Graphics]::FromImage($bitmap) $graphics.CopyFromScreen([int32]$rect.Left, [int32]$rect.Top, 0, 0, $bitmap.Size) $graphics.Dispose() $stream = New-Object IO.MemoryStream $bitmap.Save($stream, [Drawing.Imaging.ImageFormat]::Png) $bitmap.Dispose() [Convert]::ToBase64String($stream.ToArray()) `; const SLEEP_INTERVAL_MS = 500; export async function getPageSource(this: NovaWindowsDriver): Promise { return await this.sendPowerShellCommand(GET_PAGE_SOURCE_COMMAND.format(AutomationElement.automationRoot)); } export async function maximizeWindow(this: NovaWindowsDriver): Promise { const automationRoot = new FoundAutomationElement(AutomationElement.automationRoot.buildGetPropertyCommand(Property.RUNTIME_ID)); try { await this.sendPowerShellCommand(automationRoot.buildMaximizeCommand()); } catch { throw new errors.UnknownError('Failed to maximize the window.'); } } export async function minimizeWindow(this: NovaWindowsDriver): Promise { const automationRoot = new FoundAutomationElement(AutomationElement.automationRoot.buildGetPropertyCommand(Property.RUNTIME_ID)); try { await this.sendPowerShellCommand(automationRoot.buildMinimizeCommand()); } catch { throw new errors.UnknownError('Failed to minimize the window.'); } } export async function getScreenshot(this: NovaWindowsDriver): Promise { const automationRootId = await this.sendPowerShellCommand(AutomationElement.automationRoot.buildCommand()); if (this.caps.app && this.caps.app.toLowerCase() !== 'root') { try { await this.focusElement({ [W3C_ELEMENT_KEY]: automationRootId.trim(), } satisfies Element); } catch { // noop } } return await this.sendPowerShellCommand(GET_SCREENSHOT_COMMAND); } export async function getWindowRect(this: NovaWindowsDriver): Promise { const result = await this.sendPowerShellCommand(AutomationElement.automationRoot.buildGetElementRectCommand()); return JSON.parse(result.replaceAll(/(?:infinity)/gi, 0x7FFFFFFF.toString())); } export async function getWindowHandle(this: NovaWindowsDriver): Promise { const nativeWindowHandle = await this.sendPowerShellCommand(AutomationElement.automationRoot.buildGetPropertyCommand(Property.NATIVE_WINDOW_HANDLE)); return `0x${Number(nativeWindowHandle).toString(16).padStart(8, '0')}`; } export async function getWindowHandles(this: NovaWindowsDriver): Promise { const result = await this.sendPowerShellCommand(AutomationElement.rootElement.findAll(TreeScope.CHILDREN, new TrueCondition()).buildCommand()); const elIds = result.split('\n').map((x) => x.trim()).filter(Boolean); const nativeWindowHandles: string[] = []; for (const elId of elIds) { const nativeWindowHandle = await this.sendPowerShellCommand(new FoundAutomationElement(elId).buildGetPropertyCommand(Property.NATIVE_WINDOW_HANDLE)); nativeWindowHandles.push(`0x${Number(nativeWindowHandle).toString(16).padStart(8, '0')}`); } return nativeWindowHandles; } export async function setWindow(this: NovaWindowsDriver, nameOrHandle: string): Promise { const handle = Number(nameOrHandle); for (let i = 1; i <= 20; i++) { // TODO: make a setting for the number of retries or timeout if (!isNaN(handle)) { const condition = new PropertyCondition(Property.NATIVE_WINDOW_HANDLE, new PSInt32(handle)); const elementId = await this.sendPowerShellCommand(AutomationElement.rootElement.findFirst(TreeScope.CHILDREN_OR_SELF, condition).buildCommand()); if (elementId.trim() !== '') { await this.sendPowerShellCommand(/* ps1 */ `$rootElement = ${new FoundAutomationElement(elementId).buildCommand()}`); trySetForegroundWindow(handle); return; } } const name = nameOrHandle; const condition = new PropertyCondition(Property.NAME, new PSString(name)); const elementId = await this.sendPowerShellCommand(AutomationElement.rootElement.findFirst(TreeScope.CHILDREN, condition).buildCommand()); if (elementId.trim() !== '') { this.log.info(`Found window with name '${name}'. Setting it as the root element.`); await this.sendPowerShellCommand(/* ps1 */ `$rootElement = ${new FoundAutomationElement(elementId).buildCommand()}`); trySetForegroundWindow(handle); return; } this.log.info(`Failed to locate window with name '${name}'. Sleeping for ${SLEEP_INTERVAL_MS} milliseconds and retrying... (${i}/20)`); // TODO: make a setting for the number of retries or timeout await sleep(SLEEP_INTERVAL_MS); // TODO: make a setting for the sleep timeout } throw new errors.NoSuchWindowError(`No window was found with name or handle '${nameOrHandle}'.`); } export async function closeApp(this: NovaWindowsDriver): Promise { const result = await this.sendPowerShellCommand(AutomationElement.automationRoot.buildCommand()); const elementId = result.split('\n').map((id) => id.trim()).filter(Boolean)[0]; if (!elementId) { throw new errors.NoSuchWindowError('No active app window is found for this session.'); } await this.sendPowerShellCommand(new FoundAutomationElement(elementId).buildCloseCommand()); await this.sendPowerShellCommand(/* ps1 */ `$rootElement = $null`); } export async function launchApp(this: NovaWindowsDriver): Promise { if (!this.caps.app || ['root', 'none'].includes(this.caps.app.toLowerCase())) { throw new errors.InvalidArgumentError('No app capability is set for this session.'); } await this.changeRootElement(this.caps.app); } export async function changeRootElement(this: NovaWindowsDriver, path: string): Promise export async function changeRootElement(this: NovaWindowsDriver, nativeWindowHandle: number): Promise export async function changeRootElement(this: NovaWindowsDriver, pathOrNativeWindowHandle: string | number): Promise { if (typeof pathOrNativeWindowHandle === 'number') { const nativeWindowHandle = pathOrNativeWindowHandle; const condition = new PropertyCondition(Property.NATIVE_WINDOW_HANDLE, new PSInt32(nativeWindowHandle)); const elementId = await this.sendPowerShellCommand(AutomationElement.rootElement.findFirst(TreeScope.CHILDREN_OR_SELF, condition).buildCommand()); if (elementId.trim() !== '') { await this.sendPowerShellCommand(/* ps1 */ `$rootElement = ${new FoundAutomationElement(elementId).buildCommand()}`); trySetForegroundWindow(nativeWindowHandle); return; } throw new errors.UnknownError('Failed to locate top level window with that window handle.'); } const path = pathOrNativeWindowHandle; if (path.includes('!') && path.includes('_') && !(path.includes('/') || path.includes('\\'))) { this.log.debug('Detected app path to be in the UWP format.'); await this.sendPowerShellCommand(/* ps1 */ `Start-Process 'explorer.exe' 'shell:AppsFolder\\${path}'${this.caps.appArguments ? ` -ArgumentList '${this.caps.appArguments}'` : ''}`); await sleep((this.caps['ms:waitForAppLaunch'] ?? 0) * 1000 || SLEEP_INTERVAL_MS); for (let i = 1; i <= 20; i++) { const result = await this.sendPowerShellCommand(/* ps1 */ `(Get-Process -Name 'ApplicationFrameHost').Id`); const processIds = result.split('\n').map((pid) => pid.trim()).filter(Boolean).map(Number); this.log.debug('Process IDs of ApplicationFrameHost processes: ' + processIds.join(', ')); try { await this.attachToApplicationWindow(processIds); return; } catch { // noop } this.log.info(`Failed to locate window of the app. Sleeping for ${SLEEP_INTERVAL_MS} milliseconds and retrying... (${i}/20)`); // TODO: make a setting for the number of retries or timeout await sleep(SLEEP_INTERVAL_MS); // TODO: make a setting for the sleep timeout } } else { this.log.debug('Detected app path to be in the classic format.'); const normalizedPath = normalize(path); await this.sendPowerShellCommand(/* ps1 */ `Start-Process '${normalizedPath}'${this.caps.appArguments ? ` -ArgumentList '${this.caps.appArguments}'` : ''}`); await sleep((this.caps['ms:waitForAppLaunch'] ?? 0) * 1000 || 500); for (let i = 1; i <= 20; i++) { try { const breadcrumbs = normalizedPath.toLowerCase().split('\\').flatMap((x) => x.split('/')); const executable = breadcrumbs[breadcrumbs.length - 1]; const processName = executable.endsWith('.exe') ? executable.slice(0, executable.length - 4) : executable; const result = await this.sendPowerShellCommand(/* ps1 */ `(Get-Process -Name '${processName}' | Sort-Object StartTime -Descending).Id`); const processIds = result.split('\n').map((pid) => pid.trim()).filter(Boolean).map(Number); this.log.debug(`Process IDs of '${processName}' processes: ` + processIds.join(', ')); await this.attachToApplicationWindow(processIds); return; } catch (err) { if (err instanceof Error) { this.log.debug(`Received error:\n${err.message}`); } } this.log.info(`Failed to locate window of the app. Sleeping for ${SLEEP_INTERVAL_MS} milliseconds and retrying... (${i}/20)`); // TODO: make a setting for the number of retries or timeout await sleep(SLEEP_INTERVAL_MS); // TODO: make a setting for the sleep timeout } } throw new errors.UnknownError('Failed to locate window of the app.'); } export async function back(this: NovaWindowsDriver): Promise { const elementId = (await this.sendPowerShellCommand(AutomationElement.automationRoot.buildCommand())).trim(); if (!elementId) { throw new errors.NoSuchWindowError('No active window found for this session.'); } keyDown(Key.ALT); keyDown(Key.LEFT); keyUp(Key.LEFT); keyUp(Key.ALT); } export async function forward(this: NovaWindowsDriver): Promise { const elementId = (await this.sendPowerShellCommand(AutomationElement.automationRoot.buildCommand())).trim(); if (!elementId) { throw new errors.NoSuchWindowError('No active window found for this session.'); } keyDown(Key.ALT); keyDown(Key.RIGHT); keyUp(Key.RIGHT); keyUp(Key.ALT); } export async function title(this: NovaWindowsDriver): Promise { const elementId = (await this.sendPowerShellCommand(AutomationElement.automationRoot.buildCommand())).trim(); if (!elementId) { throw new errors.NoSuchWindowError('No active window found for this session.'); } return await this.sendPowerShellCommand( AutomationElement.automationRoot.buildGetPropertyCommand(Property.NAME) ); } export async function setWindowRect( this: NovaWindowsDriver, x: number | null, y: number | null, width: number | null, height: number | null ): Promise { if (width != null && width < 0) { throw new errors.InvalidArgumentError('width must be a non-negative integer.'); } if (height != null && height < 0) { throw new errors.InvalidArgumentError('height must be a non-negative integer.'); } const elementId = (await this.sendPowerShellCommand(AutomationElement.automationRoot.buildCommand())).trim(); if (!elementId) { throw new errors.NoSuchWindowError('No active window found for this session.'); } const el = new FoundAutomationElement(elementId); if (x != null && y != null) { await this.sendPowerShellCommand(el.buildMoveCommand(x, y)); } if (width != null && height != null) { await this.sendPowerShellCommand(el.buildResizeCommand(width, height)); } return await this.getWindowRect(); } export async function waitForNewWindow(this: NovaWindowsDriver, pid: number, timeout: number): Promise { const start = Date.now(); let attempts = 0; while (Date.now() - start < timeout) { const elements = await this.sendPowerShellCommand(AutomationElement.rootElement.findAll(TreeScope.CHILDREN, new PropertyCondition(Property.PROCESS_ID, new PSInt32(pid))).buildCommand()); const elementIds = elements.split('\n').map((id) => id.trim()).filter(Boolean); if (elementIds.length > 0) { const nativeWindowHandle = await this.sendPowerShellCommand(new FoundAutomationElement(elementIds[0]).buildGetPropertyCommand(Property.NATIVE_WINDOW_HANDLE)); return Number(nativeWindowHandle); } this.log.debug(`Waiting for the process window to appear... (${++attempts}/${Math.floor(timeout / SLEEP_INTERVAL_MS)})`); await sleep(SLEEP_INTERVAL_MS); } throw new errors.TimeoutError('Timed out waiting for window.'); } export async function attachToApplicationWindow(this: NovaWindowsDriver, processIds: number[]): Promise { const nativeWindowHandle = await waitForNewWindow.call(this, processIds[0], this.caps['ms:waitForAppLaunch'] ?? SLEEP_INTERVAL_MS * 20); let elementId = ''; for (let i = 1; i <= 20; i++) { elementId = await this.sendPowerShellCommand(AutomationElement.rootElement.findFirst(TreeScope.CHILDREN, new PropertyCondition(Property.NATIVE_WINDOW_HANDLE, new PSInt32(nativeWindowHandle))).buildCommand()); if (elementId) { break; } this.log.info(`The window with handle 0x${nativeWindowHandle.toString(16).padStart(8, '0')} is not yet available in the UI Automation tree. Sleeping for ${SLEEP_INTERVAL_MS} milliseconds and retrying... (${i}/20)`); // TODO: make a setting for the number of retries or timeout await sleep(SLEEP_INTERVAL_MS); // TODO: make a setting for the sleep timeout } await this.sendPowerShellCommand(/* ps1 */ `$rootElement = ${new FoundAutomationElement(elementId).buildCommand()}`); if ((await this.sendPowerShellCommand(/* ps1 */ `$null -ne $rootElement`)).toLowerCase() === 'true') { const nativeWindowHandle = Number(await this.sendPowerShellCommand(AutomationElement.automationRoot.buildGetPropertyCommand(Property.NATIVE_WINDOW_HANDLE))); if (!trySetForegroundWindow(nativeWindowHandle)) { try { await this.focusElement({ [W3C_ELEMENT_KEY]: elementId, } satisfies Element); } catch { // ignore, avoid "Target element cannot receive focus" error } }; return; } }