diff --git a/.github/workflows/lint-build.yml b/.github/workflows/lint-build.yml
index 786aaf1..54574d1 100644
--- a/.github/workflows/lint-build.yml
+++ b/.github/workflows/lint-build.yml
@@ -2,7 +2,9 @@ name: Lint & Build
on:
pull_request:
- branches: [ "main" ]
+ branches:
+ - main
+ - develop
jobs:
build:
@@ -11,12 +13,12 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Set up Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
- node-version: 18.x
+ node-version: 24.x
- name: Install dependencies
run: npm install --no-package-lock
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b69b340..1b0e746 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,18 +3,20 @@ name: Release
on:
workflow_dispatch:
push:
- branches: [ main ]
+ branches:
+ - main
+ - develop
jobs:
build:
runs-on: ubuntu-latest
environment: Release
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v5
- name: Use Node.js
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v6
with:
- node-version: lts/*
+ node-version: 24.x
- run: npm install --no-package-lock
name: Install dependencies
- run: npm run build
diff --git a/.releaserc b/.releaserc
index 6b0755e..2bd1f0a 100644
--- a/.releaserc
+++ b/.releaserc
@@ -1,4 +1,8 @@
{
+ "branches": [
+ "main",
+ { "name": "develop", "prerelease": "preview" }
+ ],
"plugins": [
["@semantic-release/commit-analyzer", {
"preset": "angular",
diff --git a/README.md b/README.md
index 56a22f9..84d80f9 100644
--- a/README.md
+++ b/README.md
@@ -11,9 +11,8 @@ It’s designed to handle real-world scenarios where traditional drivers fall sh
> **Note**
>
-> This driver is built for Appium 2 and is not compatible with Appium 1. To get started,
-> clone the repository and run `npm install` to resolve dependencies. Then, use `npm run build`
-> to build the driver. Finally, add it to your Appium 2 distribution with the command:
+> This driver is built for Appium 2/3 and is not compatible with Appium 1. To install
+> the driver, simply run:
> `appium driver install --source=npm appium-novawindows-driver`
@@ -44,6 +43,11 @@ delayBeforeClick | Time in milliseconds before a click is performed.
delayAfterClick | Time in milliseconds after a click is performed.
appTopLevelWindow | The handle of an existing application top-level window to attach to. It can be a number or string (not necessarily hexadecimal). Example: `12345`, `0x12345`.
shouldCloseApp | Whether to close the window of the application in test after the session finishes. Default is `true`.
+appArguments | Optional string of arguments to pass to the app on launch.
+appWorkingDir | Optional working directory path for the application.
+prerun | An object containing either `script` or `command` key. The value of each key must be a valid PowerShell script or command to be executed prior to the WinAppDriver session startup. See [Power Shell commands execution](#power-shell-commands-execution) for more details. Example: `{script: 'Get-Process outlook -ErrorAction SilentlyContinue'}`
+postrun | An object containing either `script` or `command` key. The value of each key must be a valid PowerShell script or command to be executed after WinAppDriver session is stopped. See [Power Shell commands execution](#power-shell-commands-execution) for more details.
+isolatedScriptExecution | Whether PowerShell scripts are executed in an isolated session. Default is `false`.
Please note that more capabilities will be added as the development of this driver progresses. Since it is still in its early stages, some features may be missing or subject to change. If you need a specific capability or encounter any issues, please feel free to open an issue.
@@ -103,8 +107,7 @@ key to the listof enabled insecure features. Refer to [Appium Security document]
It is possible to ether execute a single Power Shell command or a whole script
and get its stdout in response. If the script execution returns non-zero exit code then an exception
is going to be thrown. The exception message will contain the actual stderr. Unlike, Appium Windows Driver,
-there is no difference if you paste the script with `command` or `script` argument. For ease of use, you
-can even pass the script as a string only, it will work the same way.
+there is no difference if you paste the script with `command` or `script` argument. For ease of use, you can pass the script as a string when executing a PowerShell command directly via the driver. Note: This shorthand does not work when using the prerun or postrun capabilities, which require full object syntax.
Here's an example code of how to control the Notepad process:
```java
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 6097824..b36e703 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,12 +1,11 @@
// @ts-check
+import { defineConfig } from 'eslint/config';
import eslint from '@eslint/js';
-import { config, configs } from 'typescript-eslint';
import appiumConfig from '@appium/eslint-config-appium-ts';
-export default config(
- ...appiumConfig,
+export default defineConfig(
eslint.configs.recommended,
- configs.recommended,
+ ...appiumConfig,
);
diff --git a/lib/commands/app.ts b/lib/commands/app.ts
index fc20705..8f97548 100644
--- a/lib/commands/app.ts
+++ b/lib/commands/app.ts
@@ -23,11 +23,24 @@ import {
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)
@@ -104,12 +117,13 @@ export async function setWindow(this: NovaWindowsDriver, nameOrHandle: string):
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. Sleeping for 500 milliseconds and retrying... (${i}/20)`); // TODO: make a setting for the number of retries or timeout
+ this.log.info(`Failed to locate window with name '${name}'. Sleeping for 500 milliseconds and retrying... (${i}/20)`); // TODO: make a setting for the number of retries or timeout
await sleep(500); // TODO: make a setting for the sleep timeout
}
@@ -136,12 +150,14 @@ export async function changeRootElement(this: NovaWindowsDriver, pathOrNativeWin
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(500); // TODO: make a setting for the initial wait time
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;
@@ -153,6 +169,7 @@ export async function changeRootElement(this: NovaWindowsDriver, pathOrNativeWin
await sleep(500); // 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(500); // TODO: make a setting for the initial wait time
@@ -163,6 +180,7 @@ export async function changeRootElement(this: NovaWindowsDriver, pathOrNativeWin
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;
@@ -182,6 +200,7 @@ export async function changeRootElement(this: NovaWindowsDriver, pathOrNativeWin
export async function attachToApplicationWindow(this: NovaWindowsDriver, processIds: number[]): Promise {
const nativeWindowHandles = getWindowAllHandlesForProcessIds(processIds);
+ this.log.debug(`Detected the following native window handles for the given process IDs: ${nativeWindowHandles.map((handle) => `0x${handle.toString(16).padStart(8, '0')}`).join(', ')}`);
if (nativeWindowHandles.length !== 0) {
const elementId = await this.sendPowerShellCommand(AutomationElement.rootElement.findFirst(TreeScope.CHILDREN, new PropertyCondition(Property.NATIVE_WINDOW_HANDLE, new PSInt32(nativeWindowHandles[0]))).buildCommand());
diff --git a/lib/commands/extension.ts b/lib/commands/extension.ts
index 66216fe..e37ea69 100644
--- a/lib/commands/extension.ts
+++ b/lib/commands/extension.ts
@@ -94,7 +94,6 @@ type KeyAction = {
down?: boolean,
}
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function execute(this: NovaWindowsDriver, script: string, args: any[]) {
if (script.startsWith(PLATFORM_COMMAND_PREFIX)) {
script = script.replace(PLATFORM_COMMAND_PREFIX, '').trim();
@@ -293,7 +292,11 @@ export async function executePowerShellScript(this: NovaWindowsDriver, script: s
}
const scriptToExecute = pwsh`${script}`;
- return await this.sendPowerShellCommand(scriptToExecute);
+ if (this.caps.isolatedScriptExecution) {
+ return await this.sendIsolatedPowerShellCommand(scriptToExecute);
+ } else {
+ return await this.sendPowerShellCommand(scriptToExecute);
+ }
}
export async function executeKeys(this: NovaWindowsDriver, keyActions: { actions: KeyAction | KeyAction[], forceUnicode: boolean }) {
@@ -375,12 +378,8 @@ export async function executeClick(this: NovaWindowsDriver, clickArgs: {
interClickDelayMs = 100,
} = clickArgs;
- if (!!elementId && ((x !== null && x !== undefined) || (y !== null && y !== undefined))) {
- throw new errors.InvalidArgumentError('Either elementId or x and y must be provided.');
- }
-
- if ((x !== null && x !== undefined) !== (y !== null && y !== undefined)) {
- throw new errors.InvalidArgumentError('Both x and y must be provided.');
+ if ((x != null) !== (y != null)) {
+ throw new errors.InvalidArgumentError('Both x and y must be provided if either is set.');
}
let pos: [number, number];
@@ -396,7 +395,10 @@ export async function executeClick(this: NovaWindowsDriver, clickArgs: {
const rectJson = await this.sendPowerShellCommand(new FoundAutomationElement(elementId).buildGetElementRectCommand());
const rect = JSON.parse(rectJson.replaceAll(/(?:infinity)/gi, 0x7FFFFFFF.toString())) as Rect;
- pos = [rect.x + (rect.width / 2), rect.y + (rect.height / 2)];
+ pos = [
+ rect.x + (x ?? Math.trunc(rect.width / 2)),
+ rect.y + (y ?? Math.trunc(rect.height / 2)),
+ ];
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
pos = [x!, y!];
@@ -411,22 +413,23 @@ export async function executeClick(this: NovaWindowsDriver, clickArgs: {
};
const mouseButton: number = clickTypeToButtonMapping[button];
+ const processesModifierKeys = Array.isArray(modifierKeys) ? modifierKeys : [modifierKeys];
await mouseMoveAbsolute(pos[0], pos[1], 0);
for (let i = 0; i < times; i++) {
if (i !== 0) {
await sleep(interClickDelayMs);
}
- if (modifierKeys.includes('ctrl')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'ctrl')) {
keyDown(Key.CONTROL);
}
- if (modifierKeys.includes('alt')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'alt')) {
keyDown(Key.ALT);
}
- if (modifierKeys.includes('shift')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'shift')) {
keyDown(Key.SHIFT);
}
- if (modifierKeys.includes('win')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'win')) {
keyDown(Key.META);
}
@@ -436,16 +439,16 @@ export async function executeClick(this: NovaWindowsDriver, clickArgs: {
}
mouseUp(mouseButton);
- if (modifierKeys.includes('ctrl')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'ctrl')) {
keyUp(Key.CONTROL);
}
- if (modifierKeys.includes('alt')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'alt')) {
keyUp(Key.ALT);
}
- if (modifierKeys.includes('shift')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'shift')) {
keyUp(Key.SHIFT);
}
- if (modifierKeys.includes('win')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'win')) {
keyUp(Key.META);
}
}
@@ -462,7 +465,7 @@ export async function executeHover(this: NovaWindowsDriver, hoverArgs: {
endElementId?: string,
endX?: number,
endY?: number,
- modifierKeys?: ('shift' | 'ctrl' | 'alt' | 'win') | ('shift' | 'ctrl' | 'alt' | 'win')[], // TODO: add types
+ modifierKeys?: ('shift' | 'ctrl' | 'alt' | 'win') | ('shift' | 'ctrl' | 'alt' | 'win')[],
durationMs?: number,
}) {
const {
@@ -474,22 +477,15 @@ export async function executeHover(this: NovaWindowsDriver, hoverArgs: {
durationMs = 500,
} = hoverArgs;
- if (!!startElementId && ((startX !== null && startX !== undefined) || (startY !== null && startY !== undefined))) {
- throw new errors.InvalidArgumentError('Either startElementId or startX and startY must be provided.');
- }
-
- if (!!endElementId && ((endX !== null && endX !== undefined) || (endY !== null && endY !== undefined))) {
- throw new errors.InvalidArgumentError('Either endElementId or endX and endY must be provided.');
+ if ((startX != null) !== (startY != null)) {
+ throw new errors.InvalidArgumentError('Both startX and startY must be provided if either is set.');
}
- if ((startX !== null && startX !== undefined) !== (startY !== null && startY !== undefined)) {
- throw new errors.InvalidArgumentError('Both startX and startY must be provided.');
- }
-
- if ((endX !== null && endX !== undefined) !== (endY !== null && endY !== undefined)) {
- throw new errors.InvalidArgumentError('Both endX and endY must be provided.');
+ if ((endX != null) !== (endY != null)) {
+ throw new errors.InvalidArgumentError('Both endX and endY must be provided if either is set.');
}
+ const processesModifierKeys = Array.isArray(modifierKeys) ? modifierKeys : [modifierKeys];
let startPos: [number, number];
if (startElementId) {
if (await this.sendPowerShellCommand(/* ps1 */ `$null -eq ${new FoundAutomationElement(startElementId).toString()}`)) {
@@ -503,13 +499,15 @@ export async function executeHover(this: NovaWindowsDriver, hoverArgs: {
const rectJson = await this.sendPowerShellCommand(new FoundAutomationElement(startElementId).buildGetElementRectCommand());
const rect = JSON.parse(rectJson.replaceAll(/(?:infinity)/gi, 0x7FFFFFFF.toString())) as Rect;
- startPos = [rect.x + (rect.width / 2), rect.y + (rect.height / 2)];
+ startPos = [
+ rect.x + (startX ?? rect.width / 2),
+ rect.y + (startY ?? rect.height / 2)
+ ];
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
startPos = [startX!, startY!];
}
-
let endPos: [number, number];
if (endElementId) {
if (await this.sendPowerShellCommand(/* ps1 */ `$null -eq ${new FoundAutomationElement(endElementId).toString()}`)) {
@@ -523,7 +521,10 @@ export async function executeHover(this: NovaWindowsDriver, hoverArgs: {
const rectJson = await this.sendPowerShellCommand(new FoundAutomationElement(endElementId).buildGetElementRectCommand());
const rect = JSON.parse(rectJson.replaceAll(/(?:infinity)/gi, 0x7FFFFFFF.toString())) as Rect;
- endPos = [rect.x + (rect.width / 2), rect.y + (rect.height / 2)];
+ endPos = [
+ rect.x + (endX ?? rect.width / 2),
+ rect.y + (endY ?? rect.height / 2)
+ ];
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
endPos = [endX!, endY!];
@@ -531,31 +532,31 @@ export async function executeHover(this: NovaWindowsDriver, hoverArgs: {
await mouseMoveAbsolute(startPos[0], startPos[1], 0);
- if (modifierKeys.includes('ctrl')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'ctrl')) {
keyDown(Key.CONTROL);
}
- if (modifierKeys.includes('alt')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'alt')) {
keyDown(Key.ALT);
}
- if (modifierKeys.includes('shift')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'shift')) {
keyDown(Key.SHIFT);
}
- if (modifierKeys.includes('win')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'win')) {
keyDown(Key.META);
}
await mouseMoveAbsolute(endPos[0], endPos[1], durationMs, this.caps.smoothPointerMove);
- if (modifierKeys.includes('ctrl')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'ctrl')) {
keyUp(Key.CONTROL);
}
- if (modifierKeys.includes('alt')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'alt')) {
keyUp(Key.ALT);
}
- if (modifierKeys.includes('shift')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'shift')) {
keyUp(Key.SHIFT);
}
- if (modifierKeys.includes('win')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'win')) {
keyUp(Key.META);
}
}
@@ -583,6 +584,7 @@ export async function executeScroll(this: NovaWindowsDriver, scrollArgs: {
throw new errors.InvalidArgumentError('Both x and y must be provided.');
}
+ const processesModifierKeys = Array.isArray(modifierKeys) ? modifierKeys : [modifierKeys];
let pos: [number, number];
if (elementId) {
if (await this.sendPowerShellCommand(/* ps1 */ `$null -eq ${new FoundAutomationElement(elementId).toString()}`)) {
@@ -604,31 +606,31 @@ export async function executeScroll(this: NovaWindowsDriver, scrollArgs: {
await mouseMoveAbsolute(pos[0], pos[1], 0);
- if (modifierKeys.includes('ctrl')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'ctrl')) {
keyDown(Key.CONTROL);
}
- if (modifierKeys.includes('alt')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'alt')) {
keyDown(Key.ALT);
}
- if (modifierKeys.includes('shift')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'shift')) {
keyDown(Key.SHIFT);
}
- if (modifierKeys.includes('win')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'win')) {
keyDown(Key.META);
}
mouseScroll(deltaX ?? 0, deltaY ?? 0);
- if (modifierKeys.includes('ctrl')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'ctrl')) {
keyUp(Key.CONTROL);
}
- if (modifierKeys.includes('alt')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'alt')) {
keyUp(Key.ALT);
}
- if (modifierKeys.includes('shift')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'shift')) {
keyUp(Key.SHIFT);
}
- if (modifierKeys.includes('win')) {
+ if (processesModifierKeys.some((key) => key.toLowerCase() === 'win')) {
keyUp(Key.META);
}
}
diff --git a/lib/commands/index.ts b/lib/commands/index.ts
index 7469824..9a55d43 100644
--- a/lib/commands/index.ts
+++ b/lib/commands/index.ts
@@ -22,7 +22,6 @@ type Commands = {
};
declare module '../driver' {
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface NovaWindowsDriver extends Commands {}
}
diff --git a/lib/commands/powershell.ts b/lib/commands/powershell.ts
index 47c2ba4..c484ebe 100644
--- a/lib/commands/powershell.ts
+++ b/lib/commands/powershell.ts
@@ -8,6 +8,7 @@ const ADD_NECESSARY_ASSEMBLIES = /* ps1 */ `Add-Type -AssemblyName UIAutomationC
const USE_UI_AUTOMATION_CLIENT = /* ps1 */ `using namespace System.Windows.Automation`;
const INIT_CACHE_REQUEST = /* ps1 */ `($cacheRequest = New-Object System.Windows.Automation.CacheRequest).TreeFilter = [AndCondition]::new([Automation]::ControlViewCondition, [NotCondition]::new([PropertyCondition]::new([AutomationElement]::FrameworkIdProperty, 'Chrome'))); $cacheRequest.Push()`;
const INIT_ROOT_ELEMENT = /* ps1 */ `$rootElement = [AutomationElement]::RootElement`;
+const NULL_ROOT_ELEMENT = /* ps1 */ `$rootElement = $null`;
const INIT_ELEMENT_TABLE = /* ps1 */ `$elementTable = New-Object System.Collections.Generic.Dictionary[[string]\`,[AutomationElement]]`;
export async function startPowerShellSession(this: NovaWindowsDriver): Promise {
@@ -15,18 +16,30 @@ export async function startPowerShellSession(this: NovaWindowsDriver): Promise {
this.powerShellStdOut += chunk.toString();
});
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
powerShell.stderr.on('data', (chunk: any) => {
this.powerShellStdErr += chunk.toString();
});
this.powerShell = powerShell;
+ if (this.caps.appWorkingDir) {
+ const envVarsSet: Set = new Set();
+ const matches = this.caps.appWorkingDir.matchAll(/%([^%]+)%/g);
+
+ for (const match of matches) {
+ envVarsSet.add(match[1]);
+ }
+ const envVars = Array.from(envVarsSet);
+ for (const envVar of envVars) {
+ this.caps.appWorkingDir = this.caps.appWorkingDir.replaceAll(`%${envVar}%`, process.env[envVar.toUpperCase()] ?? '');
+ }
+ this.sendPowerShellCommand(`Set-Location -Path '${this.caps.appWorkingDir}'`);
+ }
+
await this.sendPowerShellCommand(SET_UTF8_ENCODING);
await this.sendPowerShellCommand(ADD_NECESSARY_ASSEMBLIES);
await this.sendPowerShellCommand(USE_UI_AUTOMATION_CLIENT);
@@ -37,11 +50,15 @@ export async function startPowerShellSession(this: NovaWindowsDriver): Promise = new Set();
const matches = this.caps.app.matchAll(/%([^%]+)%/g);
@@ -70,6 +87,75 @@ export async function startPowerShellSession(this: NovaWindowsDriver): Promise {
+ const magicNumber = 0xF2EE;
+
+ const powerShell = spawn('powershell.exe', ['-NoExit', '-Command', '-']);
+ try {
+ powerShell.stdout.setEncoding('utf8');
+ powerShell.stdout.setEncoding('utf8');
+
+ powerShell.stdout.on('data', (chunk: any) => {
+ this.powerShellStdOut += chunk.toString();
+ });
+
+ powerShell.stderr.on('data', (chunk: any) => {
+ this.powerShellStdErr += chunk.toString();
+ });
+
+ const result = await new Promise((resolve, reject) => {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const powerShell = this.powerShell!;
+
+ this.powerShellStdOut = '';
+ this.powerShellStdErr = '';
+
+ powerShell.stdin.write(`${SET_UTF8_ENCODING}\n`);
+ if (this.caps.appWorkingDir) {
+ const envVarsSet: Set = new Set();
+ const matches = this.caps.appWorkingDir.matchAll(/%([^%]+)%/g);
+
+ for (const match of matches) {
+ envVarsSet.add(match[1]);
+ }
+ const envVars = Array.from(envVarsSet);
+ for (const envVar of envVars) {
+ this.caps.appWorkingDir = this.caps.appWorkingDir.replaceAll(`%${envVar}%`, process.env[envVar.toUpperCase()] ?? '');
+ }
+ powerShell.stdin.write(`Set-Location -Path '${this.caps.appWorkingDir}'\n`);
+ }
+ powerShell.stdin.write(`${command}\n`);
+ powerShell.stdin.write(/* ps1 */ `Write-Output $([char]0x${magicNumber.toString(16)})\n`);
+
+ const onData: Parameters[1] = ((chunk: any) => {
+ const magicChar = String.fromCharCode(magicNumber);
+ if (chunk.toString().includes(magicChar)) {
+ powerShell.stdout.off('data', onData);
+ if (this.powerShellStdErr) {
+ reject(new errors.UnknownError(this.powerShellStdErr));
+ } else {
+ resolve(this.powerShellStdOut.replace(`${magicChar}`, '').trim());
+ }
+ }
+ }).bind(this);
+
+ powerShell.stdout.on('data', onData);
+ });
+
+ // commented out for now to avoid cluttering the logs with long command outputs
+ // this.log.debug(`PowerShell command executed:\n${command}\n\nCommand output below:\n${result}\n --------`);
+
+ return result;
+ } finally {
+ // Ensure the isolated PowerShell process is terminated
+ try {
+ powerShell.kill();
+ } catch (e) {
+ this.log.warn(`Failed to terminate isolated PowerShell process: ${e}`);
+ }
+ }
+}
+
export async function sendPowerShellCommand(this: NovaWindowsDriver, command: string): Promise {
const magicNumber = 0xF2EE;
@@ -88,7 +174,6 @@ export async function sendPowerShellCommand(this: NovaWindowsDriver, command: st
powerShell.stdin.write(`${command}\n`);
powerShell.stdin.write(/* ps1 */ `Write-Output $([char]0x${magicNumber.toString(16)})\n`);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
const onData: Parameters[1] = ((chunk: any) => {
const magicChar = String.fromCharCode(magicNumber);
if (chunk.toString().includes(magicChar)) {
diff --git a/lib/constraints.ts b/lib/constraints.ts
index fb8149e..ce24b0a 100644
--- a/lib/constraints.ts
+++ b/lib/constraints.ts
@@ -22,8 +22,20 @@ export const UI_AUTOMATION_DRIVER_CONSTRAINTS = {
isBoolean: true,
},
appArguments: {
- isString: true
+ isString: true,
+ },
+ appWorkingDir: {
+ isString: true,
},
+ prerun: {
+ isObject: true,
+ },
+ postrun: {
+ isObject: true,
+ },
+ isolatedScriptExecution: {
+ isBoolean: true,
+ }
} as const satisfies Constraints;
export default UI_AUTOMATION_DRIVER_CONSTRAINTS;
diff --git a/lib/driver.ts b/lib/driver.ts
index a44e07f..8e7a551 100644
--- a/lib/driver.ts
+++ b/lib/driver.ts
@@ -178,6 +178,11 @@ export class NovaWindowsDriver extends BaseDriver[0], string>);
+ }
+
setDpiAwareness();
this.log.debug(`Started session ${sessionId}.`);
return [sessionId, caps];
@@ -202,6 +207,12 @@ export class NovaWindowsDriver extends BaseDriver[0], string>);
+ }
+
await super.deleteSession(sessionId);
}
diff --git a/lib/enums.ts b/lib/enums.ts
index 036c77d..8b9f151 100644
--- a/lib/enums.ts
+++ b/lib/enums.ts
@@ -4,9 +4,7 @@ type Enumerate = Acc['length'] exte
type IntRange = Exclude, Enumerate> | T
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AllFlagsValue = [N] extends [Partial['length']] ? A['length'] : UnionFlags;
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
type UnionFlags = IntRange<0, AllFlagsValue>;
export type Enum = T[keyof T];
diff --git a/lib/powershell/conditions.ts b/lib/powershell/conditions.ts
index dd60bd5..83992b4 100644
--- a/lib/powershell/conditions.ts
+++ b/lib/powershell/conditions.ts
@@ -162,7 +162,6 @@ export class FalseCondition extends Condition {
}
}
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
function assertPSObjectType(obj: PSObject, type: new (...args: any[]) => PSObject) {
if (!(obj instanceof type)) {
throw new errors.InvalidArgumentError(`Property expected type ${type.name} but got ${(obj as object)?.constructor.name}.`);
diff --git a/lib/powershell/core.ts b/lib/powershell/core.ts
index 7a4613c..749aa78 100644
--- a/lib/powershell/core.ts
+++ b/lib/powershell/core.ts
@@ -20,7 +20,6 @@ export function pwsh(strings: TemplateStringsArray, ...values: string[]): string
export function pwsh$(literals: TemplateStringsArray, ...substitutions: number[]) {
const templateInstance = $(literals, ...substitutions);
const defaultFormat = templateInstance.format.bind(templateInstance);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
templateInstance.format = (...args: any[]) => {
const command = defaultFormat(...args);
return /* ps1 */ `(Invoke-Expression -Command ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${btoa(command)}'))))`;
diff --git a/lib/util.ts b/lib/util.ts
index 5ca8605..0453e98 100644
--- a/lib/util.ts
+++ b/lib/util.ts
@@ -41,7 +41,6 @@ export class DeferredStringTemplate {
});
}
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
format(...args: any[]): string {
const out: string[] = [];
for (let i = 0, k = 0; i < this.literals.length; i++, k++) {
diff --git a/package.json b/package.json
index 1f53400..dcd5845 100644
--- a/package.json
+++ b/package.json
@@ -27,12 +27,12 @@
"url": "https://github.com/AutomateThePlanet/appium-novawindows-driver/issues"
},
"peerDependencies": {
- "appium": "^2.17.1"
+ "appium": "^3.1.0"
},
"dependencies": {
- "@appium/base-driver": "^9.16.4",
+ "@appium/base-driver": "^10.1.0",
"bezier-easing": "^2.1.0",
- "koffi": "^2.11.0",
+ "koffi": "^2.14.1",
"xpath-analyzer": "^3.0.1"
},
"appium": {
@@ -44,17 +44,17 @@
"mainClass": "NovaWindowsDriver"
},
"devDependencies": {
- "@appium/eslint-config-appium-ts": "^1.0.3",
- "@appium/tsconfig": "^0.3.5",
- "@appium/types": "^0.25.2",
- "@eslint/js": "^9.25.1",
+ "@appium/eslint-config-appium-ts": "^2.0.3",
+ "@appium/tsconfig": "^1.1.0",
+ "@appium/types": "^1.1.0",
+ "@eslint/js": "^9.38.0",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
- "@types/node": "^22.14.1",
- "conventional-changelog-conventionalcommits": "^8.0.0",
- "eslint": "^9.25.1",
- "semantic-release": "^24.2.3",
- "typescript": "^5.8.3",
- "typescript-eslint": "^8.31.0"
+ "@types/node": "^24.8.1",
+ "conventional-changelog-conventionalcommits": "^9.1.0",
+ "eslint": "^9.38.0",
+ "semantic-release": "^25.0.1",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.46.1"
}
}