Skip to content

Commit ee12870

Browse files
authored
fix: changed logic of attaching the root window not working on some machines
2 parents bd21178 + 095b2af commit ee12870

File tree

10 files changed

+382
-141
lines changed

10 files changed

+382
-141
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,8 @@ build/
140140

141141
# Ignore package-lock.json in favour of npm-shrinkwrap.json for npm publishing
142142
package-lock.json*
143+
144+
# Ignore downloaded chromedriver/edgedriver/ffmpeg binaries
145+
chromedriver/
146+
edgedriver/
147+
ffmpeg/

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,13 @@ appWorkingDir | Optional working directory path for the application.
4848
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'}`
4949
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.
5050
isolatedScriptExecution | Whether PowerShell scripts are executed in an isolated session. Default is `false`.
51-
enableWebView | Whether to enable WebView support. Set to true to allow switching into WebView contexts. Default is `false`.
51+
webviewEnabled | Whether to enable WebView support. Set to true to allow switching into WebView contexts. Default is `false`.
5252
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.
53+
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`.
54+
edgedriverCdnUrl | Base URL used to download EdgeDriver binaries for automating Edge (WebView2)-based WebViews in desktop applications. Defaults to `https://msedgedriver.microsoft.com`.
55+
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.
56+
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.
57+
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.
5358

5459
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.
5560

lib/commands/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
import { sleep } from '../util';
1717
import { errors, W3C_ELEMENT_KEY } from '@appium/base-driver';
1818
import {
19-
getWindowAllHandlesForProcessIds,
19+
// getWindowAllHandlesForProcessIds,
2020
keyDown,
2121
keyUp,
2222
trySetForegroundWindow,

lib/commands/contexts.ts

Lines changed: 43 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
import { Chromedriver, ChromedriverOpts } from 'appium-chromedriver';
22
import { fs, node, system, tempDir, zip } from '@appium/support';
3-
import http from 'node:http';
4-
import https from 'node:https';
53
import path from 'node:path';
6-
import { pipeline } from 'node:stream/promises';
7-
import { sleep } from '../util';
4+
import { cdpRequest, downloadFile, sleep, MODULE_NAME } from '../util';
85
import { NovaWindowsDriver } from '../driver';
96
import { errors } from '@appium/base-driver';
107

118
const NATIVE_APP = 'NATIVE_APP';
129
const WEBVIEW = 'WEBVIEW';
1310
const WEBVIEW_BASE = `${WEBVIEW}_`;
1411

15-
const MODULE_NAME = 'appium-novawindows-driver';
16-
1712
export async function getCurrentContext(this: NovaWindowsDriver): Promise<string> {
1813
return this.currentContext ??= NATIVE_APP;
1914
}
@@ -79,6 +74,7 @@ export async function setContext(this: NovaWindowsDriver, name?: string | null):
7974
'goog:chromeOptions': options,
8075
};
8176

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

@@ -158,7 +154,7 @@ interface CDPListResponseEntry {
158154
type CDPListResponse = CDPListResponseEntry[];
159155

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

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

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

181-
const webViewDetails: WebViewDetails = {
182-
info: await cdpRequest.call(this, ({ host, port, endpoint: '/json/version', timeout: 10000 })),
183-
pages: await cdpRequest.call(this, ({ host, port, endpoint: '/json/list', timeout: 10000 })),
184-
};
177+
const info = await (cdpRequest.call(this, ({ host, port, endpoint: '/json/version', timeout: 10000 })) as Promise<CDPVersionResponse>).catch(() => undefined);
178+
const pages = await (cdpRequest.call(this, ({ host, port, endpoint: '/json/list', timeout: 10000 })) as Promise<CDPListResponse>).catch(() => undefined);
179+
180+
const webViewDetails: WebViewDetails = { info, pages };
185181

186182
return webViewDetails;
187183
}
@@ -206,28 +202,57 @@ async function getDriverExecutable(this: NovaWindowsDriver, browserType: 'Edge'
206202
await fs.mkdir(driverDir);
207203
}
208204

209-
let downloadUrl = '';
210205
const fileName = browserType === 'Edge' ? 'msedgedriver.exe' : 'chromedriver.exe';
211206
const finalPath = path.join(driverDir, browserVersion, fileName);
212207

213208
if (await fs.exists(finalPath)) {
214209
return finalPath;
215-
};
210+
}
211+
212+
const executablePath = browserType === 'Edge'
213+
? this.caps.edgedriverExecutablePath
214+
: this.caps.chromedriverExecutablePath;
215+
216+
if (executablePath) {
217+
const exists = await fs.exists(executablePath);
218+
if (!exists) {
219+
throw new errors.InvalidArgumentError(`Driver executable not found at path: ${executablePath}`);
220+
}
221+
222+
this.log.debug(
223+
`Using local ${browserType} driver executable at ${executablePath}. ` +
224+
`Automatic download is disabled and CDN URLs are ignored. ` +
225+
`You must ensure this binary matches the WebView/Chromium version (${browserVersion}).`
226+
);
227+
228+
return executablePath;
229+
}
216230

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

234+
const CHROME_BASE_URL = this.caps.chromedriverCdnUrl || 'https://storage.googleapis.com/chrome-for-testing-public';
235+
const EDGE_BASE_URL = this.caps.edgedriverCdnUrl || 'https://msedgedriver.microsoft.com';
236+
237+
let downloadUrl = '';
238+
220239
if (browserType === 'Chrome') {
221-
downloadUrl = `https://storage.googleapis.com/chrome-for-testing-public/${browserVersion}/win${arch}/${zipFilename}`;
222-
} else if (browserType === 'Edge') {
223-
downloadUrl = `https://msedgedriver.microsoft.com/${browserVersion}/${zipFilename}`;
240+
const url = new URL(CHROME_BASE_URL);
241+
url.pathname = path.posix.join(url.pathname, browserVersion, `win${arch}`, zipFilename);
242+
downloadUrl = url.toString();
243+
} else {
244+
const url = new URL(EDGE_BASE_URL);
245+
url.pathname = path.posix.join(url.pathname, browserVersion, zipFilename);
246+
downloadUrl = url.toString();
224247
}
225248

226249
this.log.debug(`Downloading ${browserType} driver version ${browserVersion}...`);
227250
const tmpRoot = await tempDir.openDir();
228251
await downloadFile(downloadUrl, tmpRoot);
252+
229253
try {
230254
await zip.extractAllTo(path.join(tmpRoot, zipFilename), tmpRoot);
255+
231256
const driverPath = await fs.walkDir(
232257
tmpRoot,
233258
true,
@@ -236,78 +261,12 @@ async function getDriverExecutable(this: NovaWindowsDriver, browserType: 'Edge'
236261
if (!driverPath) {
237262
throw new errors.UnknownError(`The archive was unzipped properly, but did not find any ${driverType} executable.`);
238263
}
264+
239265
this.log.debug(`Moving the extracted '${fileName}' to '${finalPath}'`);
240266
await fs.mv(driverPath, finalPath, { mkdirp: true });
241267
} finally {
242268
await fs.rimraf(tmpRoot);
243269
}
244-
return finalPath;
245-
}
246-
247-
async function cdpRequest<T = unknown>(this: NovaWindowsDriver, { host, port, endpoint, timeout }): Promise<T> {
248-
if (this?.log) {
249-
this.log.debug(`Sending request to ${host}:${port}${endpoint}`);
250-
}
251270

252-
return new Promise<T>((resolve, reject) => {
253-
const options = {
254-
hostname: host,
255-
port,
256-
path: endpoint,
257-
method: 'GET',
258-
agent: new http.Agent({ keepAlive: false }),
259-
timeout,
260-
};
261-
262-
const req = http.request(options, (res) => {
263-
let data = '';
264-
res.on('data', (chunk) => {
265-
data += chunk;
266-
});
267-
res.on('end', () => {
268-
try {
269-
resolve(JSON.parse(data));
270-
} catch (err) {
271-
reject(err);
272-
}
273-
});
274-
});
275-
276-
req.on('error', reject);
277-
req.on('timeout', () => {
278-
req.destroy(new Error('Request timed out'));
279-
});
280-
281-
req.end();
282-
});
283-
}
284-
285-
async function downloadFile(url: string, destPath: string, timeout = 30000): Promise<void> {
286-
const protocol = url.startsWith('https') ? https : http;
287-
const fileName = path.basename(new URL(url).pathname);
288-
289-
const fullFilePath = path.join(destPath, fileName);
290-
291-
return new Promise<void>((resolve, reject) => {
292-
const req = protocol.get(url, async (res) => {
293-
if (res.statusCode !== 200) {
294-
return reject(new Error(`Download failed: ${res.statusCode}`));
295-
}
296-
297-
try {
298-
const fileStream = fs.createWriteStream(fullFilePath);
299-
await pipeline(res, fileStream);
300-
resolve();
301-
} catch (err) {
302-
await fs.unlink(fullFilePath).catch(() => { });
303-
reject(err);
304-
}
305-
});
306-
307-
req.on('error', reject);
308-
req.setTimeout(timeout, () => {
309-
req.destroy();
310-
reject(new Error(`Timeout downloading from ${url}`));
311-
});
312-
});
271+
return finalPath;
313272
}

lib/commands/extension.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export async function execute(this: NovaWindowsDriver, script: string, args: any
120120
}
121121

122122
if (script === 'mobile:getContexts') {
123-
if (!this.caps.enableWebView) {
123+
if (!this.caps.webviewEnabled) {
124124
throw new errors.InvalidArgumentError('WebView support is not enabled. To use this command, enable WebView support by setting the "enableWebView" capability to true.');
125125
}
126126
const { waitForWebviewMs }: { waitForWebviewMs?: number } = args[0] || {};
@@ -720,7 +720,7 @@ export async function startRecordingScreen(this: NovaWindowsDriver, args?: {
720720
}
721721
}
722722
const videoPath = outputPath ?? join(tmpdir(), `novawindows-recording-${Date.now()}.${DEFAULT_EXT}`);
723-
this._screenRecorder = new ScreenRecorder(videoPath, this.log, {
723+
this._screenRecorder = new ScreenRecorder(videoPath, this, {
724724
fps: fps !== undefined ? parseInt(String(fps), 10) : undefined,
725725
timeLimit: timeLimit !== undefined ? parseInt(String(timeLimit), 10) : undefined,
726726
preset,

lib/commands/powershell.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export async function startPowerShellSession(this: NovaWindowsDriver): Promise<v
8080
this.caps.app = this.caps.app.replaceAll(`%${envVar}%`, process.env[envVar.toUpperCase()] ?? '');
8181
}
8282

83-
if (this.caps.enableWebView) {
83+
if (this.caps.webviewEnabled) {
8484
this.webviewDevtoolsPort = this.caps.webviewDevtoolsPort
8585
? Number(this.caps.webviewDevtoolsPort)
8686
: await findFreePort(DEFAULT_WEBVIEW_DEVTOOLS_PORT_LOWER, DEFAULT_WEBVIEW_DEVTOOLS_PORT_UPPER);

0 commit comments

Comments
 (0)