Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3a1a630
Merge pull request #39 from AutomateThePlanet/releases/v1.2.0
teo-nikolov Oct 20, 2025
f9cb5c5
chore(release): 1.2.0-preview.1 [skip ci]
semantic-release-bot Oct 20, 2025
32dc094
Merge pull request #41 from AutomateThePlanet/releases/v1.2.0
teo-nikolov Nov 11, 2025
7109367
Merge pull request #42 from AutomateThePlanet/releases/v1.2.0
teo-nikolov Nov 12, 2025
59b68f7
Merge pull request #43 from AutomateThePlanet/releases/v1.2.0
teo-nikolov Nov 12, 2025
6ad9bdb
chore(release): 1.2.0-preview.2 [skip ci]
semantic-release-bot Apr 2, 2026
e92c60a
Merge main into develop [skip ci]
teo-nikolov Apr 2, 2026
578e07e
Merge branch 'develop' of github.com:AutomateThePlanet/appium-novawin…
teo-nikolov Apr 2, 2026
644034f
feat(webview): enable WebView2 support
teo-nikolov Apr 2, 2026
ee0314d
Merge pull request #71 from AutomateThePlanet/feature/webview-support
teo-nikolov Apr 2, 2026
7880d26
chore(release): 1.4.0-preview.1 [skip ci]
semantic-release-bot Apr 2, 2026
fe3f762
fix: debug cdp json issue
teo-nikolov Apr 2, 2026
3bd5408
chore(release): 1.4.0-preview.2 [skip ci]
semantic-release-bot Apr 2, 2026
37b9291
fix: fixed port cdpRequest issue
teo-nikolov Apr 2, 2026
be8d889
Merge branch 'develop' of github.com:AutomateThePlanet/appium-novawin…
teo-nikolov Apr 2, 2026
e6542fb
docs(readme): added enableWebView to documentation
teo-nikolov Apr 2, 2026
fa4c019
Merge pull request #72 from AutomateThePlanet/feature/webview-support
teo-nikolov Apr 2, 2026
9bd5189
chore(release): 1.4.0-preview.3 [skip ci]
semantic-release-bot Apr 2, 2026
b013bc8
added minimize and maximize command
teo-nikolov Apr 9, 2026
49aee1c
changed finding new applicaiton window logic
teo-nikolov Apr 9, 2026
ef6ba5c
Merge pull request #73 from AutomateThePlanet/feature/webview-support
teo-nikolov Apr 9, 2026
00edf24
fix(debug): changed logic for finding window on app launch
teo-nikolov Apr 9, 2026
ca4408f
Merge pull request #74 from AutomateThePlanet/feature/webview-support
teo-nikolov Apr 9, 2026
bd21178
chore(release): 1.4.0-preview.4 [skip ci]
semantic-release-bot Apr 9, 2026
c15567c
fix(webview): fix error when no webview endpoint is available
teo-nikolov Apr 9, 2026
a08f775
fix(webview): fix current webview not being set
teo-nikolov Apr 9, 2026
095b2af
feat: making screen recorder ffmpeg auto-downloadable and updated web…
teo-nikolov Apr 14, 2026
ee12870
fix: changed logic of attaching the root window not working on some m…
teo-nikolov Apr 14, 2026
b4757b2
chore(release): 1.4.0-preview.5 [skip ci]
semantic-release-bot Apr 14, 2026
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,8 @@ build/

# Ignore package-lock.json in favour of npm-shrinkwrap.json for npm publishing
package-lock.json*

# Ignore downloaded chromedriver/edgedriver/ffmpeg binaries
chromedriver/
edgedriver/
ffmpeg/
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,44 @@
## [1.4.0-preview.5](https://github.com/AutomateThePlanet/appium-novawindows-driver/compare/v1.4.0-preview.4...v1.4.0-preview.5) (2026-04-14)

### Features

* making screen recorder ffmpeg auto-downloadable and updated webview capability names ([095b2af](https://github.com/AutomateThePlanet/appium-novawindows-driver/commit/095b2af7d5a85dd757e92d5666d19ed2a20699b3))

### Bug Fixes

* changed logic of attaching the root window not working on some machines ([ee12870](https://github.com/AutomateThePlanet/appium-novawindows-driver/commit/ee12870d4f83a1b830c18b65f9dc1a7493825f96))
* **webview:** fix current webview not being set ([a08f775](https://github.com/AutomateThePlanet/appium-novawindows-driver/commit/a08f775f935b90660e1229c1ed294822a93a024e))
* **webview:** fix error when no webview endpoint is available ([c15567c](https://github.com/AutomateThePlanet/appium-novawindows-driver/commit/c15567c6d06db5b1581afd441c1a1eb7887ee1b8))

## [1.4.0-preview.4](https://github.com/AutomateThePlanet/appium-novawindows-driver/compare/v1.4.0-preview.3...v1.4.0-preview.4) (2026-04-09)

### Bug Fixes

* **debug:** changed logic for finding window on app launch ([00edf24](https://github.com/AutomateThePlanet/appium-novawindows-driver/commit/00edf24d751672123eec0cb6db0b3c34a4bfd51c))

## [1.4.0-preview.3](https://github.com/AutomateThePlanet/appium-novawindows-driver/compare/v1.4.0-preview.2...v1.4.0-preview.3) (2026-04-02)

### Bug Fixes

* fixed port cdpRequest issue ([37b9291](https://github.com/AutomateThePlanet/appium-novawindows-driver/commit/37b9291369c30ff2a39f7618d5d20d3b9725d9a0))

## [1.4.0-preview.2](https://github.com/AutomateThePlanet/appium-novawindows-driver/compare/v1.4.0-preview.1...v1.4.0-preview.2) (2026-04-02)

### Bug Fixes

* debug cdp json issue ([fe3f762](https://github.com/AutomateThePlanet/appium-novawindows-driver/commit/fe3f762ae666fc9de5158f03c2b7c9acb62c52c0))

## [1.4.0-preview.1](https://github.com/AutomateThePlanet/appium-novawindows-driver/compare/v1.3.1...v1.4.0-preview.1) (2026-04-02)

### Features

* **webview:** enable WebView2 support ([644034f](https://github.com/AutomateThePlanet/appium-novawindows-driver/commit/644034f866efcca48b87e0344b88d34f4471f608))

### Miscellaneous Chores

* **release:** 1.2.0-preview.1 [skip ci] ([f9cb5c5](https://github.com/AutomateThePlanet/appium-novawindows-driver/commit/f9cb5c5c8e7e88b7929171fe39c59635b721f4de))
* **release:** 1.2.0-preview.2 [skip ci] ([6ad9bdb](https://github.com/AutomateThePlanet/appium-novawindows-driver/commit/6ad9bdbdbe26cb5c7f881a8e1e80f064dfc6863b))

## [1.3.1](https://github.com/AutomateThePlanet/appium-novawindows-driver/compare/v1.3.0...v1.3.1) (2026-03-09)

### Bug Fixes
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ 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`.
webviewEnabled | Whether to enable WebView support. Set to true to allow switching into WebView contexts. Default is `false`.
webviewDevtoolsPort | The local port number to use for devtools communication. By default the first free port from 10900..11000 range is selected. Set a custom port if running parallel tests or when app is "none"/"root"/appTopLevelWindow is specified.
chromedriverCdnUrl | Base URL used to download ChromeDriver binaries for automating Chromium-based WebViews in desktop applications. Defaults to `https://storage.googleapis.com/chrome-for-testing-public`.
edgedriverCdnUrl | Base URL used to download EdgeDriver binaries for automating Edge (WebView2)-based WebViews in desktop applications. Defaults to `https://msedgedriver.microsoft.com`.
edgedriverExecutablePath | Absolute file path to a locally provided Microsoft Edge WebDriver binary. When this is set, automatic download via edgedriverCdnUrl is disabled and the provided executable is used directly. The binary must be explicitly supplied by the user (e.g. downloaded manually or stored in CI artifacts). It is the user’s responsibility to ensure the driver version matches the installed Edge / WebView2 runtime version used by the target environment, otherwise automation may fail due to version incompatibility.
chromedriverExecutablePath | Absolute file path to a locally provided ChromeDriver binary. When this is set, automatic download via chromedriverCdnUrl is disabled and the provided executable is used directly. The binary must be explicitly supplied (manually downloaded or managed externally, such as in CI). The user must ensure version compatibility between ChromeDriver and the target Chromium / WebView version, as mismatches can break automation.
ffmpegExecutablePath | Absolute file path to a locally provided FFmpeg executable binary. When this is set, automatic download of FFmpeg is disabled and the provided executable is used directly. The binary must be supplied manually (e.g. downloaded and stored in CI artifacts or bundled externally). It is the user’s responsibility to ensure the FFmpeg build is compatible with the target Windows environment. If the path is invalid or the file does not exist, execution will fail with an error.

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
31 changes: 25 additions & 6 deletions lib/commands/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { sleep } from '../util';
import { errors, W3C_ELEMENT_KEY } from '@appium/base-driver';
import {
getWindowAllHandlesForProcessIds,
// getWindowAllHandlesForProcessIds,
keyDown,
keyUp,
trySetForegroundWindow,
Expand Down Expand Up @@ -64,6 +64,24 @@ export async function getPageSource(this: NovaWindowsDriver): Promise<string> {
return await this.sendPowerShellCommand(GET_PAGE_SOURCE_COMMAND.format(AutomationElement.automationRoot));
}

export async function maximizeWindow(this: NovaWindowsDriver): Promise<void> {
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<void> {
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<string> {
const automationRootId = await this.sendPowerShellCommand(AutomationElement.automationRoot.buildCommand());

Expand Down Expand Up @@ -287,17 +305,18 @@ export async function waitForNewWindow(this: NovaWindowsDriver, pid: number, tim
let attempts = 0;

while (Date.now() - start < timeout) {
const handles = getWindowAllHandlesForProcessIds([pid]);

if (handles.length > 0) {
return handles[handles.length - 1];
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 Error('Timed out waiting for window.');
throw new errors.TimeoutError('Timed out waiting for window.');
}

export async function attachToApplicationWindow(this: NovaWindowsDriver, processIds: number[]): Promise<void> {
Expand Down
272 changes: 272 additions & 0 deletions lib/commands/contexts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import { Chromedriver, ChromedriverOpts } from 'appium-chromedriver';
import { fs, node, system, tempDir, zip } from '@appium/support';
import path from 'node:path';
import { cdpRequest, downloadFile, sleep, MODULE_NAME } from '../util';
import { NovaWindowsDriver } from '../driver';
import { errors } from '@appium/base-driver';

const NATIVE_APP = 'NATIVE_APP';
const WEBVIEW = 'WEBVIEW';
const WEBVIEW_BASE = `${WEBVIEW}_`;

export async function getCurrentContext(this: NovaWindowsDriver): Promise<string> {
return this.currentContext ??= NATIVE_APP;
}

export async function setContext(this: NovaWindowsDriver, name?: string | null): Promise<void> {
if (!name || name === NATIVE_APP) {
this.chromedriver?.stop();
this.chromedriver = null;
this.jwpProxyActive = false;
this.proxyReqRes = null;
this.proxyCommand = null;
this.currentContext = NATIVE_APP;
return;
}

const webViewDetails = await this.getWebViewDetails();

if (!(webViewDetails.pages ?? []).map((page) => page.id).includes(name.replace(WEBVIEW_BASE, ''))) {
throw new errors.InvalidArgumentError(`Web view not found: ${name}`);
}

const browser = webViewDetails.info?.Browser ?? '';
const match = browser.match(/(Chrome|Edg)\/([\d.]+)/);

if (!match?.[1] || (match[1] !== 'Edg' && match[1] !== 'Chrome')) {
throw new errors.InvalidArgumentError(`Unsupported browser type: ${match?.[1]}`);
}

const browserType = match[1] === 'Edg' ? 'Edge' : 'Chrome';
const browserVersion = match?.[2] ?? '';

const DRIVER_VERSION_REGEX = /^\d+(\.\d+){3}$/;
if (!DRIVER_VERSION_REGEX.test(browserVersion)) {
throw new errors.InvalidArgumentError(`Invalid browser version: ${browserVersion}`);
}

this.log.debug(`Type: ${browserType}, Version: ${browserVersion}`);

const executable: string = await getDriverExecutable.call(this, browserType, browserVersion);

const chromedriverOpts: ChromedriverOpts & { details?: WebViewDetails } = {
executable,
details: webViewDetails,
};

if (this.basePath) {
chromedriverOpts.reqBasePath = this.basePath;
}

const cd = new Chromedriver(chromedriverOpts);
this.chromedriver = cd;

const page = webViewDetails.pages?.find((p) => p.id === name.replace(WEBVIEW_BASE, ''));

const debuggerAddress = (page?.webSocketDebuggerUrl ?? '')
.replace('ws://', '')
.split('/')[0];

const options = { debuggerAddress };

const caps = {
'ms:edgeOptions': options,
'goog:chromeOptions': options,
};

this.currentContext = name;
await this.chromedriver.start(caps);
this.log.debug('Chromedriver started. Session ID:', cd.sessionId());

this.proxyReqRes = this.chromedriver.proxyReq.bind(this.chromedriver);
this.proxyCommand = this.chromedriver.jwproxy.command.bind(this.chromedriver.jwproxy);
this.jwpProxyActive = true;
}

export async function getContexts(this: NovaWindowsDriver): Promise<string[]> {
const webViewDetails = await this.getWebViewDetails();
return [
NATIVE_APP,
...(webViewDetails.pages?.map((page) => `${WEBVIEW_BASE}${page.id}`) ?? []),
];
}

export interface WebViewDetails {
/**
* Web view details as returned by /json/version CDP endpoint
* @example
* {
* "Browser": "Edg/145.0.3800.97",
* "Protocol-Version": "1.3",
* "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0",
* "V8-Version": "14.5.40.9",
* "WebKit-Version": "537.36 (@f4c49d5241f148220b99eb7f045ac370a1694a15)",
* "webSocketDebuggerUrl": "ws://localhost:10900/devtools/browser/7039e1b9-f75d-44eb-8583-7279c107bb18"
* }
*/
info?: CDPVersionResponse;

/**
* Web view details as returned by /json/list CDP endpoint
* @example // TODO: change example to not include Spotify / use mock data
* [ {
* "description": "",
* "devtoolsFrontendUrl": "https://chrome-devtools-frontend.appspot.com/serve_rev/@80be69ef794ba862ff256b0b23f051cbbc32e1ed/inspector.html?ws=localhost:9222/devtools/page/21C6035BC3E0A67D0BB6AE10F4A66D4A",
* "faviconUrl": "https://xpui.app.spotify.com/favicon.ico",
* "id": "21C6035BC3E0A67D0BB6AE10F4A66D4A",
* "title": "Spotify - Web Player: Music for everyone",
* "type": "page",
* "url": "https://xpui.app.spotify.com/index.html",
* "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/21C6035BC3E0A67D0BB6AE10F4A66D4A"
* }, {
* "description": "",
* "devtoolsFrontendUrl": "https://chrome-devtools-frontend.appspot.com/serve_rev/@80be69ef794ba862ff256b0b23f051cbbc32e1ed/inspector.html?ws=localhost:9222/devtools/page/7E7008B3C464CD91224ADF976115101F",
* "id": "7E7008B3C464CD91224ADF976115101F",
* "title": "",
* "type": "worker",
* "url": "",
* "webSocketDebuggerUrl": "ws://localhost:10900/devtools/page/7E7008B3C464CD91224ADF976115101F"
* } ]
*/
pages?: CDPListResponse;
}

interface CDPVersionResponse {
'Browser': string,
'Protocol-Version': string,
'User-Agent': string,
'V8-Version': string,
'WebKit-Version': string,
'webSocketDebuggerUrl': string,
}

interface CDPListResponseEntry {
'description': string,
'devtoolsFrontendUrl': string,
'faviconUrl': string,
'id': string,
'title': string,
'type': string,
'url': string,
'webSocketDebuggerUrl': string,
}

type CDPListResponse = CDPListResponseEntry[];

export async function getWebViewDetails(this: NovaWindowsDriver, waitForWebviewMs?: number): Promise<WebViewDetails> {
if (!this.caps.webviewEnabled) {
throw new errors.InvalidArgumentError('WebView support is not enabled. Please set the "enableWebView" capability to true and try again.');
}

this.log.debug(`Getting a list of available webviews`);

const waitMs = waitForWebviewMs ? Number(waitForWebviewMs) : 0;
if (waitMs) {
this.log.debug(`waiting for ${waitMs} ms`);
await sleep(waitMs);
}

const host = 'localhost';

if ((this.caps.app === 'none' || this.caps.app === 'root' || this.caps.appTopLevelWindow != null) && this.caps.webviewDevtoolsPort == null) {
throw new errors.InvalidArgumentError(`Capability "webviewDevtoolsPort" must be set when using "none", "root", or "appTopLevelWindow" with "enableWebView"`);
}

const port = this.webviewDevtoolsPort ??= this.caps.webviewDevtoolsPort ?? null;

const info = await (cdpRequest.call(this, ({ host, port, endpoint: '/json/version', timeout: 10000 })) as Promise<CDPVersionResponse>).catch(() => undefined);
const pages = await (cdpRequest.call(this, ({ host, port, endpoint: '/json/list', timeout: 10000 })) as Promise<CDPListResponse>).catch(() => undefined);

const webViewDetails: WebViewDetails = { info, pages };

return webViewDetails;
}

async function getDriverExecutable(this: NovaWindowsDriver, browserType: 'Edge' | 'Chrome', browserVersion: `${number}.${number}.${number}.${number}`): Promise<string> {
let driverType: string;

if (browserType === 'Chrome') {
driverType = 'chromedriver';
} else {
driverType = 'edgedriver';
}

const root = node.getModuleRootSync(MODULE_NAME, __filename);
if (!root) {
throw new errors.InvalidArgumentError(`Cannot find the root folder of the ${MODULE_NAME} Node.js module`);
}

const driverDir = path.join(root, driverType);

if (!(await fs.exists(driverDir))) {
await fs.mkdir(driverDir);
}

const fileName = browserType === 'Edge' ? 'msedgedriver.exe' : 'chromedriver.exe';
const finalPath = path.join(driverDir, browserVersion, fileName);

if (await fs.exists(finalPath)) {
return finalPath;
}

const executablePath = browserType === 'Edge'
? this.caps.edgedriverExecutablePath
: this.caps.chromedriverExecutablePath;

if (executablePath) {
const exists = await fs.exists(executablePath);
if (!exists) {
throw new errors.InvalidArgumentError(`Driver executable not found at path: ${executablePath}`);
}

this.log.debug(
`Using local ${browserType} driver executable at ${executablePath}. ` +
`Automatic download is disabled and CDN URLs are ignored. ` +
`You must ensure this binary matches the WebView/Chromium version (${browserVersion}).`
);

return executablePath;
}

const arch = await system.arch();
const zipFilename = `${driverType}${browserType === 'Edge' ? '_' : '-'}win${arch}.zip`;

const CHROME_BASE_URL = this.caps.chromedriverCdnUrl || 'https://storage.googleapis.com/chrome-for-testing-public';
const EDGE_BASE_URL = this.caps.edgedriverCdnUrl || 'https://msedgedriver.microsoft.com';

let downloadUrl = '';

if (browserType === 'Chrome') {
const url = new URL(CHROME_BASE_URL);
url.pathname = path.posix.join(url.pathname, browserVersion, `win${arch}`, zipFilename);
downloadUrl = url.toString();
} else {
const url = new URL(EDGE_BASE_URL);
url.pathname = path.posix.join(url.pathname, browserVersion, zipFilename);
downloadUrl = url.toString();
}

this.log.debug(`Downloading ${browserType} driver version ${browserVersion}...`);
const tmpRoot = await tempDir.openDir();
await downloadFile(downloadUrl, tmpRoot);

try {
await zip.extractAllTo(path.join(tmpRoot, zipFilename), tmpRoot);

const driverPath = await fs.walkDir(
tmpRoot,
true,
(itemPath, isDirectory) => !isDirectory && path.parse(itemPath).base.toLowerCase() === fileName);

if (!driverPath) {
throw new errors.UnknownError(`The archive was unzipped properly, but did not find any ${driverType} executable.`);
}

this.log.debug(`Moving the extracted '${fileName}' to '${finalPath}'`);
await fs.mv(driverPath, finalPath, { mkdirp: true });
} finally {
await fs.rimraf(tmpRoot);
}

return finalPath;
}
Loading