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
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/
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +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`.
enableWebView | Whether to enable WebView support. Set to true to allow switching into WebView contexts. 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
2 changes: 1 addition & 1 deletion 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
127 changes: 43 additions & 84 deletions lib/commands/contexts.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { Chromedriver, ChromedriverOpts } from 'appium-chromedriver';
import { fs, node, system, tempDir, zip } from '@appium/support';
import http from 'node:http';
import https from 'node:https';
import path from 'node:path';
import { pipeline } from 'node:stream/promises';
import { sleep } from '../util';
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}_`;

const MODULE_NAME = 'appium-novawindows-driver';

export async function getCurrentContext(this: NovaWindowsDriver): Promise<string> {
return this.currentContext ??= NATIVE_APP;
}
Expand Down Expand Up @@ -79,6 +74,7 @@ export async function setContext(this: NovaWindowsDriver, name?: string | null):
'goog:chromeOptions': options,
};

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

Expand Down Expand Up @@ -158,7 +154,7 @@ interface CDPListResponseEntry {
type CDPListResponse = CDPListResponseEntry[];

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

Expand All @@ -178,10 +174,10 @@ export async function getWebViewDetails(this: NovaWindowsDriver, waitForWebviewM

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

const webViewDetails: WebViewDetails = {
info: await cdpRequest.call(this, ({ host, port, endpoint: '/json/version', timeout: 10000 })),
pages: await cdpRequest.call(this, ({ host, port, endpoint: '/json/list', timeout: 10000 })),
};
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;
}
Expand All @@ -206,28 +202,57 @@ async function getDriverExecutable(this: NovaWindowsDriver, browserType: 'Edge'
await fs.mkdir(driverDir);
}

let downloadUrl = '';
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') {
downloadUrl = `https://storage.googleapis.com/chrome-for-testing-public/${browserVersion}/win${arch}/${zipFilename}`;
} else if (browserType === 'Edge') {
downloadUrl = `https://msedgedriver.microsoft.com/${browserVersion}/${zipFilename}`;
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,
Expand All @@ -236,78 +261,12 @@ async function getDriverExecutable(this: NovaWindowsDriver, browserType: 'Edge'
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;
}

async function cdpRequest<T = unknown>(this: NovaWindowsDriver, { host, port, endpoint, timeout }): Promise<T> {
if (this?.log) {
this.log.debug(`Sending request to ${host}:${port}${endpoint}`);
}

return new Promise<T>((resolve, reject) => {
const options = {
hostname: host,
port,
path: endpoint,
method: 'GET',
agent: new http.Agent({ keepAlive: false }),
timeout,
};

const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (err) {
reject(err);
}
});
});

req.on('error', reject);
req.on('timeout', () => {
req.destroy(new Error('Request timed out'));
});

req.end();
});
}

async function downloadFile(url: string, destPath: string, timeout = 30000): Promise<void> {
const protocol = url.startsWith('https') ? https : http;
const fileName = path.basename(new URL(url).pathname);

const fullFilePath = path.join(destPath, fileName);

return new Promise<void>((resolve, reject) => {
const req = protocol.get(url, async (res) => {
if (res.statusCode !== 200) {
return reject(new Error(`Download failed: ${res.statusCode}`));
}

try {
const fileStream = fs.createWriteStream(fullFilePath);
await pipeline(res, fileStream);
resolve();
} catch (err) {
await fs.unlink(fullFilePath).catch(() => { });
reject(err);
}
});

req.on('error', reject);
req.setTimeout(timeout, () => {
req.destroy();
reject(new Error(`Timeout downloading from ${url}`));
});
});
return finalPath;
}
4 changes: 2 additions & 2 deletions lib/commands/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export async function execute(this: NovaWindowsDriver, script: string, args: any
}

if (script === 'mobile:getContexts') {
if (!this.caps.enableWebView) {
if (!this.caps.webviewEnabled) {
throw new errors.InvalidArgumentError('WebView support is not enabled. To use this command, enable WebView support by setting the "enableWebView" capability to true.');
}
const { waitForWebviewMs }: { waitForWebviewMs?: number } = args[0] || {};
Expand Down Expand Up @@ -720,7 +720,7 @@ export async function startRecordingScreen(this: NovaWindowsDriver, args?: {
}
}
const videoPath = outputPath ?? join(tmpdir(), `novawindows-recording-${Date.now()}.${DEFAULT_EXT}`);
this._screenRecorder = new ScreenRecorder(videoPath, this.log, {
this._screenRecorder = new ScreenRecorder(videoPath, this, {
fps: fps !== undefined ? parseInt(String(fps), 10) : undefined,
timeLimit: timeLimit !== undefined ? parseInt(String(timeLimit), 10) : undefined,
preset,
Expand Down
2 changes: 1 addition & 1 deletion lib/commands/powershell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export async function startPowerShellSession(this: NovaWindowsDriver): Promise<v
this.caps.app = this.caps.app.replaceAll(`%${envVar}%`, process.env[envVar.toUpperCase()] ?? '');
}

if (this.caps.enableWebView) {
if (this.caps.webviewEnabled) {
this.webviewDevtoolsPort = this.caps.webviewDevtoolsPort
? Number(this.caps.webviewDevtoolsPort)
: await findFreePort(DEFAULT_WEBVIEW_DEVTOOLS_PORT_LOWER, DEFAULT_WEBVIEW_DEVTOOLS_PORT_UPPER);
Expand Down
Loading
Loading