Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/lint-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ name: Lint & Build

on:
pull_request:
branches: [ "main" ]
branches:
- main
- develop

jobs:
build:
Expand All @@ -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
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions .releaserc
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"branches": [
"main",
{ "name": "develop", "prerelease": "preview" }
],
"plugins": [
["@semantic-release/commit-analyzer", {
"preset": "angular",
Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`


Expand All @@ -25,7 +24,7 @@ Beside of standard Appium requirements NovaWindows Driver adds the following pre

> **Note**
>
> The driver is currently uses a PowerShell session as a back-end, and
> The driver currently uses a PowerShell session as a back-end, and
> should not require Developer Mode to be on, or any other software.
> There's a plan to update to a better, .NET-based backend for improved
> realiability and better code and error management, as well as supporting
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -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,
);
32 changes: 30 additions & 2 deletions lib/commands/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,24 @@ import {
const GET_PAGE_SOURCE_COMMAND = pwsh$ /* ps1 */ `
$el = ${0}

if ($el -eq $null) {
$dummy = [xml]'<DummyRoot></DummyRoot>'
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)

Expand Down Expand Up @@ -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
}

Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -182,9 +200,19 @@ export async function changeRootElement(this: NovaWindowsDriver, pathOrNativeWin

export async function attachToApplicationWindow(this: NovaWindowsDriver, processIds: number[]): Promise<void> {
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());
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(nativeWindowHandles[0]))).buildCommand());
if (elementId) {
break;
}
this.log.info(`The window with handle 0x${nativeWindowHandles[0].toString(16).padStart(8, '0')} is not yet available in the UI Automation tree. 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
}

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)));
Expand Down
Loading