craftdriver 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +1 -0
- package/dist/cli/dispatcher.js +1 -1
- package/dist/cli/dispatcher.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/browser.d.ts +147 -21
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +380 -139
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/capabilities.d.ts +35 -0
- package/dist/lib/capabilities.d.ts.map +1 -0
- package/dist/lib/capabilities.js +58 -0
- package/dist/lib/capabilities.js.map +1 -0
- package/dist/lib/chrome.d.ts +7 -2
- package/dist/lib/chrome.d.ts.map +1 -1
- package/dist/lib/chrome.js +10 -1
- package/dist/lib/chrome.js.map +1 -1
- package/dist/lib/driverManager.d.ts +92 -0
- package/dist/lib/driverManager.d.ts.map +1 -1
- package/dist/lib/driverManager.js +229 -10
- package/dist/lib/driverManager.js.map +1 -1
- package/dist/lib/electron.d.ts +60 -0
- package/dist/lib/electron.d.ts.map +1 -0
- package/dist/lib/electron.js +85 -0
- package/dist/lib/electron.js.map +1 -0
- package/dist/lib/electronDeeplink.d.ts +22 -0
- package/dist/lib/electronDeeplink.d.ts.map +1 -0
- package/dist/lib/electronDeeplink.js +118 -0
- package/dist/lib/electronDeeplink.js.map +1 -0
- package/dist/lib/electronDiagnostics.d.ts +49 -0
- package/dist/lib/electronDiagnostics.d.ts.map +1 -0
- package/dist/lib/electronDiagnostics.js +195 -0
- package/dist/lib/electronDiagnostics.js.map +1 -0
- package/dist/lib/electronDialogMock.d.ts +51 -0
- package/dist/lib/electronDialogMock.d.ts.map +1 -0
- package/dist/lib/electronDialogMock.js +106 -0
- package/dist/lib/electronDialogMock.js.map +1 -0
- package/dist/lib/electronFuses.d.ts +13 -0
- package/dist/lib/electronFuses.d.ts.map +1 -0
- package/dist/lib/electronFuses.js +118 -0
- package/dist/lib/electronFuses.js.map +1 -0
- package/dist/lib/electronMainBridge.d.ts +28 -0
- package/dist/lib/electronMainBridge.d.ts.map +1 -0
- package/dist/lib/electronMainBridge.js +270 -0
- package/dist/lib/electronMainBridge.js.map +1 -0
- package/dist/lib/electronMainLogs.d.ts +81 -0
- package/dist/lib/electronMainLogs.d.ts.map +1 -0
- package/dist/lib/electronMainLogs.js +176 -0
- package/dist/lib/electronMainLogs.js.map +1 -0
- package/dist/lib/electronMock.d.ts +52 -0
- package/dist/lib/electronMock.d.ts.map +1 -0
- package/dist/lib/electronMock.js +96 -0
- package/dist/lib/electronMock.js.map +1 -0
- package/dist/lib/electronRemote.d.ts +106 -0
- package/dist/lib/electronRemote.d.ts.map +1 -0
- package/dist/lib/electronRemote.js +406 -0
- package/dist/lib/electronRemote.js.map +1 -0
- package/dist/lib/electronVersions.d.ts +34 -0
- package/dist/lib/electronVersions.d.ts.map +1 -0
- package/dist/lib/electronVersions.js +90 -0
- package/dist/lib/electronVersions.js.map +1 -0
- package/dist/lib/errors.d.ts +27 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +27 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/launchTarget.d.ts +30 -0
- package/dist/lib/launchTarget.d.ts.map +1 -0
- package/dist/lib/launchTarget.js +112 -0
- package/dist/lib/launchTarget.js.map +1 -0
- package/dist/lib/page.d.ts +1 -0
- package/dist/lib/page.d.ts.map +1 -1
- package/dist/lib/page.js +1 -1
- package/dist/lib/page.js.map +1 -1
- package/dist/lib/service.d.ts +21 -0
- package/dist/lib/service.d.ts.map +1 -1
- package/dist/lib/service.js +94 -6
- package/dist/lib/service.js.map +1 -1
- package/docs/api-reference.md +37 -2
- package/docs/browser-api.md +7 -1
- package/docs/electron.md +419 -0
- package/docs/error-codes.md +5 -0
- package/docs/index.md +4 -0
- package/docs/recipes/electron-app-from-another-repo.md +136 -0
- package/docs/recipes/electron-deep-link.md +117 -0
- package/docs/recipes/electron-mock-apis.md +88 -0
- package/docs/recipes/electron-native-dialog.md +105 -0
- package/docs/recipes.md +28 -9
- package/package.json +5 -1
package/dist/lib/browser.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { Builder } from './builder.js';
|
|
2
2
|
import { ChromeService } from './chrome.js';
|
|
3
|
+
import { ElectronService, writeElectronDebug } from './electron.js';
|
|
4
|
+
import { diagnoseElectronLaunchFailure } from './electronDiagnostics.js';
|
|
5
|
+
import { ElectronRemote } from './electronRemote.js';
|
|
6
|
+
import { findFreePort } from './service.js';
|
|
3
7
|
import { FirefoxService } from './firefox.js';
|
|
4
8
|
import { resolveBrowserBinaryPath } from './driverManager.js';
|
|
9
|
+
import { buildLaunchCapabilities } from './capabilities.js';
|
|
10
|
+
import { resolveLaunchTarget } from './launchTarget.js';
|
|
5
11
|
import { By } from './by.js';
|
|
6
12
|
import { until } from './wait.js';
|
|
7
13
|
import { DEFAULT_ELEMENT_TIMEOUT_MS, DEFAULT_NAVIGATION_TIMEOUT_MS, STATE_POLL_INTERVAL_MS, NETWORK_IDLE_SETTLE_MS, PORT_RELEASE_DELAY_MS, BIDI_CONNECT_MAX_ATTEMPTS, BIDI_CONNECT_BACKOFF_STEP_MS, EVAL_REALM_RETRY_ATTEMPTS, EVAL_REALM_RETRY_DELAY_MS, } from './timing.js';
|
|
@@ -18,7 +24,7 @@ import { Mouse } from './mouse.js';
|
|
|
18
24
|
import { ActionsBuilder } from './actions.js';
|
|
19
25
|
import { BiDiSession, SessionStateManager, } from './bidi/index.js';
|
|
20
26
|
import { Frame } from './frame.js';
|
|
21
|
-
import { Page } from './page.js';
|
|
27
|
+
import { Page, isNoSuchWindowError } from './page.js';
|
|
22
28
|
import { bidiWaitFor } from './loadState.js';
|
|
23
29
|
import { BrowserContext } from './browserContext.js';
|
|
24
30
|
import { Tracer } from './tracing.js';
|
|
@@ -63,6 +69,75 @@ export const devices = {
|
|
|
63
69
|
userAgent: 'Mozilla/5.0 (iPad; CPU OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1',
|
|
64
70
|
},
|
|
65
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a `goog:chromeOptions.mobileEmulation` payload from the public
|
|
74
|
+
* `MobileEmulation | DeviceName` option — a built-in device name maps to
|
|
75
|
+
* Chrome's `deviceName`, otherwise a custom `deviceMetrics`/`userAgent` config
|
|
76
|
+
* is built. Kept next to `devices`; `buildLaunchCapabilities` takes the result.
|
|
77
|
+
*/
|
|
78
|
+
function resolveMobileEmulationConfig(mobileEmulation) {
|
|
79
|
+
const emulation = typeof mobileEmulation === 'string' ? devices[mobileEmulation] : mobileEmulation;
|
|
80
|
+
if ('deviceName' in emulation && emulation.deviceName) {
|
|
81
|
+
// Use Chrome's built-in device name
|
|
82
|
+
return { deviceName: emulation.deviceName };
|
|
83
|
+
}
|
|
84
|
+
// Build custom mobile emulation config
|
|
85
|
+
const mobileConfig = {};
|
|
86
|
+
if (emulation.deviceMetrics) {
|
|
87
|
+
mobileConfig.deviceMetrics = {
|
|
88
|
+
width: emulation.deviceMetrics.width,
|
|
89
|
+
height: emulation.deviceMetrics.height,
|
|
90
|
+
pixelRatio: emulation.deviceMetrics.pixelRatio,
|
|
91
|
+
mobile: emulation.deviceMetrics.mobile ?? true,
|
|
92
|
+
touch: emulation.deviceMetrics.touch ?? true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (emulation.userAgent) {
|
|
96
|
+
mobileConfig.userAgent = emulation.userAgent;
|
|
97
|
+
}
|
|
98
|
+
return mobileConfig;
|
|
99
|
+
}
|
|
100
|
+
async function resolveElectronAppBinaryPath(appBinaryPath) {
|
|
101
|
+
const resolved = path.resolve(appBinaryPath);
|
|
102
|
+
let stat;
|
|
103
|
+
try {
|
|
104
|
+
stat = await fs.stat(resolved);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const code = error.code;
|
|
108
|
+
if (code === 'ENOENT') {
|
|
109
|
+
throw new Error(`electron.appBinaryPath "${appBinaryPath}" does not exist.`);
|
|
110
|
+
}
|
|
111
|
+
if (code === 'EACCES') {
|
|
112
|
+
throw new Error(`electron.appBinaryPath "${appBinaryPath}" is not accessible.`);
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
if (!stat.isFile()) {
|
|
117
|
+
throw new Error(`electron.appBinaryPath "${appBinaryPath}" is not a file.`);
|
|
118
|
+
}
|
|
119
|
+
if (process.platform !== 'win32') {
|
|
120
|
+
try {
|
|
121
|
+
await fs.access(resolved, fsSync.constants.X_OK);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new Error(`electron.appBinaryPath "${appBinaryPath}" is not executable.`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return resolved;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Pure predicate behind {@link Browser.waitForPage}'s matcher form: do a page's
|
|
131
|
+
* `url`/`title` satisfy the matcher? Only the fields named in `matcher` are checked,
|
|
132
|
+
* and every named field must match (string = substring, RegExp = test). Exported for
|
|
133
|
+
* unit testing.
|
|
134
|
+
*/
|
|
135
|
+
export function matchPageFields(fields, matcher) {
|
|
136
|
+
const hit = (value, pattern) => pattern === undefined ||
|
|
137
|
+
(value !== undefined &&
|
|
138
|
+
(typeof pattern === 'string' ? value.includes(pattern) : pattern.test(value)));
|
|
139
|
+
return hit(fields.url, matcher.url) && hit(fields.title, matcher.title);
|
|
140
|
+
}
|
|
66
141
|
// ---------------------------------------------------------------------------
|
|
67
142
|
// BiDi value helpers — used only by Browser.evaluate()
|
|
68
143
|
// ---------------------------------------------------------------------------
|
|
@@ -93,7 +168,10 @@ function serializeLocalValue(v) {
|
|
|
93
168
|
if (typeof v === 'object') {
|
|
94
169
|
return {
|
|
95
170
|
type: 'object',
|
|
96
|
-
value: Object.entries(v).map(([k, val]) => [
|
|
171
|
+
value: Object.entries(v).map(([k, val]) => [
|
|
172
|
+
k,
|
|
173
|
+
serializeLocalValue(val),
|
|
174
|
+
]),
|
|
97
175
|
};
|
|
98
176
|
}
|
|
99
177
|
throw new CraftdriverError(ErrorCode.EVAL_BAD_ARG, `evaluate() argument of type "${typeof v}" is not JSON-serializable. ` +
|
|
@@ -101,11 +179,16 @@ function serializeLocalValue(v) {
|
|
|
101
179
|
}
|
|
102
180
|
function unwrapRemoteValue(v) {
|
|
103
181
|
switch (v.type) {
|
|
104
|
-
case 'undefined':
|
|
105
|
-
|
|
106
|
-
case '
|
|
107
|
-
|
|
108
|
-
case '
|
|
182
|
+
case 'undefined':
|
|
183
|
+
return undefined;
|
|
184
|
+
case 'null':
|
|
185
|
+
return null;
|
|
186
|
+
case 'string':
|
|
187
|
+
return v.value;
|
|
188
|
+
case 'boolean':
|
|
189
|
+
return v.value;
|
|
190
|
+
case 'bigint':
|
|
191
|
+
return BigInt(v.value);
|
|
109
192
|
case 'number': {
|
|
110
193
|
if (v.value === 'NaN')
|
|
111
194
|
return NaN;
|
|
@@ -117,13 +200,15 @@ function unwrapRemoteValue(v) {
|
|
|
117
200
|
return -0;
|
|
118
201
|
return v.value;
|
|
119
202
|
}
|
|
120
|
-
case 'array':
|
|
203
|
+
case 'array':
|
|
204
|
+
return (v.value ?? []).map(unwrapRemoteValue);
|
|
121
205
|
case 'object':
|
|
122
206
|
return Object.fromEntries((v.value ?? []).map(([k, val]) => [
|
|
123
207
|
typeof k === 'string' ? k : String(unwrapRemoteValue(k)),
|
|
124
208
|
unwrapRemoteValue(val),
|
|
125
209
|
]));
|
|
126
|
-
case 'date':
|
|
210
|
+
case 'date':
|
|
211
|
+
return new Date(v.value);
|
|
127
212
|
case 'function':
|
|
128
213
|
throw new CraftdriverError(ErrorCode.EVAL_BAD_ARG, 'evaluate() returned a function, which is not JSON-serializable. ' +
|
|
129
214
|
'Return a primitive, array, or plain object instead.', { detail: { returnedType: 'function' } });
|
|
@@ -190,11 +275,19 @@ export class Browser {
|
|
|
190
275
|
_browserInitScriptIds = new Set();
|
|
191
276
|
_downloadsDir;
|
|
192
277
|
_driverService;
|
|
278
|
+
/** Main-process inspector endpoint, set when launched with `electron.mainProcess`. */
|
|
279
|
+
_electronInspect;
|
|
280
|
+
/** The launched Electron app's executable path (for deep-link routing on Windows). */
|
|
281
|
+
_electronAppBinaryPath;
|
|
282
|
+
_electron;
|
|
193
283
|
_browserName = 'chrome';
|
|
194
284
|
/** Active emulation overrides, re-applied to new top-level contexts. */
|
|
195
285
|
_emulation = {};
|
|
196
286
|
/** Mutable browser-level defaults. Use setDefaultTimeout() / setDefaultNavigationTimeout() to change. */
|
|
197
|
-
defaults = {
|
|
287
|
+
defaults = {
|
|
288
|
+
timeout: DEFAULT_ELEMENT_TIMEOUT_MS,
|
|
289
|
+
navigationTimeout: DEFAULT_NAVIGATION_TIMEOUT_MS,
|
|
290
|
+
};
|
|
198
291
|
constructor(driver) {
|
|
199
292
|
this.driver = driver;
|
|
200
293
|
this.keyboard = new Keyboard(this.driver);
|
|
@@ -219,102 +312,91 @@ export class Browser {
|
|
|
219
312
|
hasBrowserInitScripts = () => this._browserInitScriptIds.size > 0;
|
|
220
313
|
hasDefaultNavigationInitScripts = () => this._defaultContext?._hasInitScriptsForNavigation() ?? this.hasBrowserInitScripts();
|
|
221
314
|
static async launch(options = {}) {
|
|
222
|
-
|
|
223
|
-
|
|
315
|
+
// Normalize first: callers may be JavaScript or JSON-driven, so reject
|
|
316
|
+
// malformed/mixed targets before touching disk or starting a driver.
|
|
317
|
+
const target = resolveLaunchTarget(options);
|
|
318
|
+
const isElectron = target.kind === 'electron';
|
|
319
|
+
const name = target.browserName;
|
|
224
320
|
const isChromeFamily = name === 'chrome' || name === 'chromium';
|
|
225
|
-
if (options.mobileEmulation && !isChromeFamily) {
|
|
321
|
+
if (target.kind === 'browser' && options.mobileEmulation && !isChromeFamily) {
|
|
226
322
|
throw new Error(`mobileEmulation is only supported on Chrome/Chromium (got "${name}"). ` +
|
|
227
323
|
`Firefox does not expose a device-emulation API equivalent to goog:chromeOptions.mobileEmulation.`);
|
|
228
324
|
}
|
|
229
|
-
|
|
325
|
+
const bidiRequested = target.bidiRequested;
|
|
326
|
+
// Handle headless mode via env var (never applied to Electron — see capabilities.ts).
|
|
230
327
|
const headlessEnv = process.env.HEADLESS;
|
|
231
328
|
const isHeadless = headlessEnv === 'true' || headlessEnv === '1';
|
|
329
|
+
// Resolve Electron before creating session directories. A malformed or
|
|
330
|
+
// missing executable must never degrade into a regular Chrome launch.
|
|
331
|
+
const electronBinary = target.kind === 'electron'
|
|
332
|
+
? await resolveElectronAppBinaryPath(target.appBinaryPath)
|
|
333
|
+
: undefined;
|
|
334
|
+
if (options.electronService !== undefined &&
|
|
335
|
+
!(options.electronService instanceof ElectronService)) {
|
|
336
|
+
throw new Error('electronService must be an ElectronService instance.');
|
|
337
|
+
}
|
|
338
|
+
if (target.kind === 'electron') {
|
|
339
|
+
writeElectronDebug({
|
|
340
|
+
appPath: electronBinary,
|
|
341
|
+
protocol: bidiRequested ? 'bidi' : 'classic',
|
|
342
|
+
platform: `${os.platform()}-${os.arch()}`,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
232
345
|
// Set up downloads directory
|
|
233
|
-
const downloadsDir = options.downloadsDir ??
|
|
346
|
+
const downloadsDir = options.downloadsDir ??
|
|
347
|
+
path.join(os.tmpdir(), 'craftdriver-downloads', `session-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
234
348
|
await fs.mkdir(downloadsDir, { recursive: true });
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
// vars in
|
|
239
|
-
// conventions other tools may
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
width: emulation.deviceMetrics.width,
|
|
268
|
-
height: emulation.deviceMetrics.height,
|
|
269
|
-
pixelRatio: emulation.deviceMetrics.pixelRatio,
|
|
270
|
-
mobile: emulation.deviceMetrics.mobile ?? true,
|
|
271
|
-
touch: emulation.deviceMetrics.touch ?? true,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
if (emulation.userAgent) {
|
|
275
|
-
mobileConfig.userAgent = emulation.userAgent;
|
|
276
|
-
}
|
|
277
|
-
chromeOptions.mobileEmulation = mobileConfig;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
caps['goog:chromeOptions'] = chromeOptions;
|
|
281
|
-
}
|
|
282
|
-
else if (isFirefox) {
|
|
283
|
-
const firefoxArgs = [];
|
|
284
|
-
if (isHeadless)
|
|
285
|
-
firefoxArgs.push('-headless');
|
|
286
|
-
if (options.args?.length)
|
|
287
|
-
firefoxArgs.push(...options.args);
|
|
288
|
-
caps['moz:firefoxOptions'] = {
|
|
289
|
-
args: firefoxArgs,
|
|
290
|
-
prefs: {
|
|
291
|
-
'browser.download.folderList': 2,
|
|
292
|
-
'browser.download.dir': downloadsDir,
|
|
293
|
-
'browser.download.useDownloadDir': true,
|
|
294
|
-
'browser.helperApps.neverAsk.saveToDisk': 'application/octet-stream,application/pdf,text/plain,text/csv,application/zip',
|
|
295
|
-
'pdfjs.disabled': true,
|
|
296
|
-
},
|
|
297
|
-
...(browserPath ? { binary: browserPath } : {}),
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
if (options.enableBiDi !== false) {
|
|
301
|
-
// Request BiDi WebSocket URL
|
|
302
|
-
caps.webSocketUrl = true;
|
|
303
|
-
// Set all prompt types to 'ignore' so BiDi events fire and we can handle them
|
|
304
|
-
caps.unhandledPromptBehavior = {
|
|
305
|
-
alert: 'ignore',
|
|
306
|
-
confirm: 'ignore',
|
|
307
|
-
prompt: 'ignore',
|
|
308
|
-
beforeUnload: 'ignore',
|
|
309
|
-
};
|
|
310
|
-
}
|
|
349
|
+
// The binary that becomes goog:chromeOptions.binary / moz:firefoxOptions.binary.
|
|
350
|
+
// For Electron it's the app executable (explicit, required). For browsers
|
|
351
|
+
// it's the optional custom-binary resolution chain — but see
|
|
352
|
+
// resolveBrowserBinaryPath's doc: some of the env vars in that chain aren't
|
|
353
|
+
// craftdriver-opt-in, they're ambient conventions other tools may have set.
|
|
354
|
+
const browserBinary = target.kind === 'electron'
|
|
355
|
+
? electronBinary
|
|
356
|
+
: resolveBrowserBinaryPath(isChromeFamily ? 'chrome' : 'firefox', target.browserPath);
|
|
357
|
+
// Main-process access (opt-in): launch the Electron app with a Node inspector
|
|
358
|
+
// on a free local port. chromedriver forwards this arg to the app, enabling
|
|
359
|
+
// its main-process inspector; browser.electron connects below for log capture
|
|
360
|
+
// and retries on first executeMain() if that eager connection was too early.
|
|
361
|
+
let electronInspect;
|
|
362
|
+
let launchArgs = target.args;
|
|
363
|
+
if (target.kind === 'electron' && target.mainProcess) {
|
|
364
|
+
const host = '127.0.0.1';
|
|
365
|
+
const port = await findFreePort();
|
|
366
|
+
electronInspect = { host, port };
|
|
367
|
+
launchArgs = [...(target.args ?? []), `--inspect=${host}:${port}`];
|
|
368
|
+
}
|
|
369
|
+
const caps = buildLaunchCapabilities({
|
|
370
|
+
browserName: name,
|
|
371
|
+
isElectron,
|
|
372
|
+
isHeadless,
|
|
373
|
+
bidiRequested,
|
|
374
|
+
browserBinary,
|
|
375
|
+
downloadsDir,
|
|
376
|
+
args: launchArgs,
|
|
377
|
+
mobileEmulation: target.kind === 'browser' && isChromeFamily && options.mobileEmulation
|
|
378
|
+
? resolveMobileEmulationConfig(options.mobileEmulation)
|
|
379
|
+
: undefined,
|
|
380
|
+
});
|
|
311
381
|
const builder = new Builder().forBrowser(name);
|
|
312
382
|
let driverService;
|
|
313
|
-
if (
|
|
383
|
+
if (target.kind === 'electron') {
|
|
384
|
+
driverService =
|
|
385
|
+
options.electronService ??
|
|
386
|
+
new ElectronService({
|
|
387
|
+
chromedriverPath: target.chromedriverPath,
|
|
388
|
+
version: target.version,
|
|
389
|
+
// Lets the driver resolver read the Electron version from a packaged app's
|
|
390
|
+
// bundle when no explicit version/driver is given (production apps).
|
|
391
|
+
appBinaryPath: electronBinary,
|
|
392
|
+
});
|
|
393
|
+
builder.setChromeService(driverService);
|
|
394
|
+
}
|
|
395
|
+
else if (isChromeFamily) {
|
|
314
396
|
// Only thread browserPath into a default ChromeService — a
|
|
315
397
|
// caller-supplied one is expected to already pin what it needs (same
|
|
316
398
|
// reasoning as explicit config always winning over auto-detection).
|
|
317
|
-
driverService = options.chromeService ?? new ChromeService({ browserPath });
|
|
399
|
+
driverService = options.chromeService ?? new ChromeService({ browserPath: browserBinary });
|
|
318
400
|
builder.setChromeService(driverService);
|
|
319
401
|
}
|
|
320
402
|
else {
|
|
@@ -322,14 +404,50 @@ export class Browser {
|
|
|
322
404
|
builder.setFirefoxService(driverService);
|
|
323
405
|
}
|
|
324
406
|
builder.withCapabilities(caps);
|
|
325
|
-
|
|
407
|
+
let driver;
|
|
408
|
+
if (target.kind === 'electron') {
|
|
409
|
+
try {
|
|
410
|
+
driver = await builder.build();
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
// Turn an opaque "Chrome instance exited" into a diagnosed, actionable
|
|
414
|
+
// error (macOS signing/Gatekeeper, Linux sandbox) with the chromedriver
|
|
415
|
+
// output tail attached. Non-app-exit failures pass through unchanged.
|
|
416
|
+
throw diagnoseElectronLaunchFailure(err, {
|
|
417
|
+
appBinaryPath: electronBinary,
|
|
418
|
+
args: target.args,
|
|
419
|
+
driverOutputTail: driverService.getOutputTail?.(),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
driver = await builder.build();
|
|
425
|
+
}
|
|
326
426
|
const browser = new Browser(driver);
|
|
327
427
|
browser._downloadsDir = downloadsDir;
|
|
328
428
|
browser._driverService = driverService;
|
|
329
429
|
browser._browserName = name;
|
|
430
|
+
browser._electronInspect = electronInspect;
|
|
431
|
+
browser._electronAppBinaryPath = electronBinary;
|
|
432
|
+
// With main-process access enabled, open the inspector bridge now so
|
|
433
|
+
// main-process console/error capture (browser.electron.mainLogs) starts at
|
|
434
|
+
// launch, mirroring always-on renderer log capture. Best-effort: a disabled
|
|
435
|
+
// EnableNodeCliInspectArguments fuse or an unreachable inspector must never
|
|
436
|
+
// fail renderer automation — executeMain still surfaces the actionable error
|
|
437
|
+
// on first use.
|
|
438
|
+
if (electronInspect) {
|
|
439
|
+
const remote = new ElectronRemote(electronInspect, { appBinaryPath: electronBinary });
|
|
440
|
+
browser._electron = remote;
|
|
441
|
+
await remote.connect().catch((err) => {
|
|
442
|
+
writeElectronDebug({
|
|
443
|
+
event: 'main-bridge-eager-connect-failed',
|
|
444
|
+
error: err instanceof Error ? err.message : String(err),
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
}
|
|
330
448
|
// Initialize BiDi session if WebSocket URL available
|
|
331
449
|
const wsUrl = driver.__wsUrl;
|
|
332
|
-
if (wsUrl &&
|
|
450
|
+
if (wsUrl && bidiRequested) {
|
|
333
451
|
await browser.initBiDi(wsUrl);
|
|
334
452
|
}
|
|
335
453
|
// Load session state if provided
|
|
@@ -448,6 +566,26 @@ export class Browser {
|
|
|
448
566
|
}
|
|
449
567
|
return this._a11y;
|
|
450
568
|
}
|
|
569
|
+
/**
|
|
570
|
+
* Electron **main-process** access. `browser.electron.executeMain(fn, ...args)`
|
|
571
|
+
* runs `fn(electron, ...args)` in the app's main process (full `electron`
|
|
572
|
+
* module), complementing the renderer automation on `Browser` itself.
|
|
573
|
+
*
|
|
574
|
+
* Requires launching with `electron: { mainProcess: true }`; otherwise
|
|
575
|
+
* `executeMain` throws `ELECTRON_MAIN_UNAVAILABLE`.
|
|
576
|
+
*
|
|
577
|
+
* ```ts
|
|
578
|
+
* const version = await browser.electron.executeMain((electron) => electron.app.getVersion());
|
|
579
|
+
* ```
|
|
580
|
+
*/
|
|
581
|
+
get electron() {
|
|
582
|
+
if (!this._electron) {
|
|
583
|
+
this._electron = new ElectronRemote(this._electronInspect, {
|
|
584
|
+
appBinaryPath: this._electronAppBinaryPath,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return this._electron;
|
|
588
|
+
}
|
|
451
589
|
/**
|
|
452
590
|
* Save current session state (cookies + localStorage) to file
|
|
453
591
|
*/
|
|
@@ -660,8 +798,10 @@ export class Browser {
|
|
|
660
798
|
else {
|
|
661
799
|
if (typeof coords.latitude !== 'number' ||
|
|
662
800
|
typeof coords.longitude !== 'number' ||
|
|
663
|
-
coords.latitude < -90 ||
|
|
664
|
-
coords.
|
|
801
|
+
coords.latitude < -90 ||
|
|
802
|
+
coords.latitude > 90 ||
|
|
803
|
+
coords.longitude < -180 ||
|
|
804
|
+
coords.longitude > 180) {
|
|
665
805
|
throw new Error(`setGeolocation: invalid coordinates ${JSON.stringify(coords)}. ` +
|
|
666
806
|
'latitude must be in [-90, 90] and longitude in [-180, 180].');
|
|
667
807
|
}
|
|
@@ -836,7 +976,7 @@ export class Browser {
|
|
|
836
976
|
}
|
|
837
977
|
// Classic fallback: best-effort 500ms settle
|
|
838
978
|
else {
|
|
839
|
-
await new Promise(r => setTimeout(r, NETWORK_IDLE_SETTLE_MS));
|
|
979
|
+
await new Promise((r) => setTimeout(r, NETWORK_IDLE_SETTLE_MS));
|
|
840
980
|
}
|
|
841
981
|
return;
|
|
842
982
|
}
|
|
@@ -881,7 +1021,7 @@ export class Browser {
|
|
|
881
1021
|
const readyState = await this.driver.executeScript('return document.readyState', []);
|
|
882
1022
|
if (readyState === 'complete' || (target === 'interactive' && readyState === 'interactive'))
|
|
883
1023
|
return;
|
|
884
|
-
await new Promise(r => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
1024
|
+
await new Promise((r) => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
885
1025
|
}
|
|
886
1026
|
throw new CraftdriverError(ErrorCode.TIMEOUT_WAITING_LOAD, `waitForLoadState('${state}') timed out after ${timeout}ms`, { detail: { state, timeout } });
|
|
887
1027
|
}
|
|
@@ -901,6 +1041,10 @@ export class Browser {
|
|
|
901
1041
|
// Whether this session had a live BiDi WebSocket. Only the Firefox+BiDi
|
|
902
1042
|
// combination needs the port-release pause below; capture it before close().
|
|
903
1043
|
const bidiWasActive = !!this.bidiSession;
|
|
1044
|
+
// Close the Electron main-process inspector bridge, if one was opened.
|
|
1045
|
+
if (this._electron) {
|
|
1046
|
+
await this._electron.close().catch(() => { });
|
|
1047
|
+
}
|
|
904
1048
|
// Close BiDi connection first
|
|
905
1049
|
if (this.bidiSession) {
|
|
906
1050
|
await this.bidiSession.close().catch(() => { });
|
|
@@ -1015,7 +1159,7 @@ export class Browser {
|
|
|
1015
1159
|
catch (err) {
|
|
1016
1160
|
if (attempt < EVAL_REALM_RETRY_ATTEMPTS &&
|
|
1017
1161
|
String(err?.message).includes('execution contexts cleared')) {
|
|
1018
|
-
await new Promise(r => setTimeout(r, EVAL_REALM_RETRY_DELAY_MS));
|
|
1162
|
+
await new Promise((r) => setTimeout(r, EVAL_REALM_RETRY_DELAY_MS));
|
|
1019
1163
|
continue;
|
|
1020
1164
|
}
|
|
1021
1165
|
throw err;
|
|
@@ -1054,9 +1198,7 @@ export class Browser {
|
|
|
1054
1198
|
'BiDi is enabled by default — check that your browser supports it.');
|
|
1055
1199
|
}
|
|
1056
1200
|
const conn = this.bidiSession.getConnection();
|
|
1057
|
-
const fnSrc = typeof fnOrSrc === 'function'
|
|
1058
|
-
? fnOrSrc.toString()
|
|
1059
|
-
: `() => { ${fnOrSrc} }`;
|
|
1201
|
+
const fnSrc = typeof fnOrSrc === 'function' ? fnOrSrc.toString() : `() => { ${fnOrSrc} }`;
|
|
1060
1202
|
const result = await conn.send('script.addPreloadScript', {
|
|
1061
1203
|
functionDeclaration: fnSrc,
|
|
1062
1204
|
});
|
|
@@ -1079,7 +1221,9 @@ export class Browser {
|
|
|
1079
1221
|
if (!this.bidiSession?.isConnected()) {
|
|
1080
1222
|
throw new Error('waitForRequest() requires BiDi. BiDi is enabled by default — check your browser supports it.');
|
|
1081
1223
|
}
|
|
1082
|
-
return this.network.waitForRequest(pattern, {
|
|
1224
|
+
return this.network.waitForRequest(pattern, {
|
|
1225
|
+
timeout: opts?.timeout ?? this.defaults.navigationTimeout,
|
|
1226
|
+
});
|
|
1083
1227
|
}
|
|
1084
1228
|
/**
|
|
1085
1229
|
* Wait for the first completed response matching a URL glob or predicate.
|
|
@@ -1098,7 +1242,9 @@ export class Browser {
|
|
|
1098
1242
|
if (!this.bidiSession?.isConnected()) {
|
|
1099
1243
|
throw new Error('waitForResponse() requires BiDi. BiDi is enabled by default \u2014 check your browser supports it.');
|
|
1100
1244
|
}
|
|
1101
|
-
return this.network.waitForResponse(pattern, {
|
|
1245
|
+
return this.network.waitForResponse(pattern, {
|
|
1246
|
+
timeout: opts?.timeout ?? this.defaults.navigationTimeout,
|
|
1247
|
+
});
|
|
1102
1248
|
}
|
|
1103
1249
|
on(event, listener) {
|
|
1104
1250
|
if (!this.bidiSession?.isConnected()) {
|
|
@@ -1132,7 +1278,7 @@ export class Browser {
|
|
|
1132
1278
|
const deadline = Date.now() + timeout;
|
|
1133
1279
|
while (Date.now() < deadline) {
|
|
1134
1280
|
const after = fsSync.readdirSync(dir);
|
|
1135
|
-
const newFiles = after.filter(f => !before.has(f) && !f.endsWith('.crdownload'));
|
|
1281
|
+
const newFiles = after.filter((f) => !before.has(f) && !f.endsWith('.crdownload'));
|
|
1136
1282
|
if (newFiles.length > 0) {
|
|
1137
1283
|
const filename = newFiles[0];
|
|
1138
1284
|
const filePath = path.join(dir, filename);
|
|
@@ -1144,7 +1290,7 @@ export class Browser {
|
|
|
1144
1290
|
},
|
|
1145
1291
|
};
|
|
1146
1292
|
}
|
|
1147
|
-
await new Promise(r => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
1293
|
+
await new Promise((r) => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
1148
1294
|
}
|
|
1149
1295
|
throw new Error(`waitForDownload() timed out after ${timeout}ms — no file appeared in ${dir}`);
|
|
1150
1296
|
}
|
|
@@ -1173,7 +1319,9 @@ export class Browser {
|
|
|
1173
1319
|
}
|
|
1174
1320
|
// Classic: no push events — callers must use the imperative API below
|
|
1175
1321
|
// Return a no-op unsubscribe
|
|
1176
|
-
return () => {
|
|
1322
|
+
return () => {
|
|
1323
|
+
/* no-op */
|
|
1324
|
+
};
|
|
1177
1325
|
}
|
|
1178
1326
|
/**
|
|
1179
1327
|
* Accept the currently open dialog (OK / confirm).
|
|
@@ -1231,7 +1379,10 @@ export class Browser {
|
|
|
1231
1379
|
const timeout = opts?.timeout ?? this.defaults.timeout;
|
|
1232
1380
|
return new Promise((resolve, reject) => {
|
|
1233
1381
|
const tid = timeout > 0
|
|
1234
|
-
? setTimeout(() => {
|
|
1382
|
+
? setTimeout(() => {
|
|
1383
|
+
off();
|
|
1384
|
+
reject(new Error(`waitForDialog timed out after ${timeout}ms`));
|
|
1385
|
+
}, timeout)
|
|
1235
1386
|
: undefined;
|
|
1236
1387
|
const off = this.onDialog((dialog) => {
|
|
1237
1388
|
if (tid !== undefined)
|
|
@@ -1249,8 +1400,12 @@ export class Browser {
|
|
|
1249
1400
|
type: () => _type,
|
|
1250
1401
|
message: () => _message,
|
|
1251
1402
|
defaultValue: () => _defaultValue,
|
|
1252
|
-
accept: async (text) => {
|
|
1253
|
-
|
|
1403
|
+
accept: async (text) => {
|
|
1404
|
+
await this.acceptDialog(text);
|
|
1405
|
+
},
|
|
1406
|
+
dismiss: async () => {
|
|
1407
|
+
await this.dismissDialog();
|
|
1408
|
+
},
|
|
1254
1409
|
};
|
|
1255
1410
|
}
|
|
1256
1411
|
// ─── Frames ───────────────────────────────────────────────────────────────
|
|
@@ -1276,7 +1431,9 @@ export class Browser {
|
|
|
1276
1431
|
const topContextId = this.bidiSession.getContext();
|
|
1277
1432
|
if (topContextId) {
|
|
1278
1433
|
try {
|
|
1279
|
-
const tree = await conn.send('browsingContext.getTree', {
|
|
1434
|
+
const tree = await conn.send('browsingContext.getTree', {
|
|
1435
|
+
root: topContextId,
|
|
1436
|
+
});
|
|
1280
1437
|
// The BiDi context for the iframe has the top-level context as ancestor.
|
|
1281
1438
|
// We find the first child context (iframes appear as children).
|
|
1282
1439
|
const iframeSrc = await el.getAttribute('src');
|
|
@@ -1304,7 +1461,9 @@ export class Browser {
|
|
|
1304
1461
|
let tree;
|
|
1305
1462
|
if (conn && topContextId) {
|
|
1306
1463
|
try {
|
|
1307
|
-
tree = await conn.send('browsingContext.getTree', {
|
|
1464
|
+
tree = await conn.send('browsingContext.getTree', {
|
|
1465
|
+
root: topContextId,
|
|
1466
|
+
});
|
|
1308
1467
|
}
|
|
1309
1468
|
catch {
|
|
1310
1469
|
tree = undefined;
|
|
@@ -1328,7 +1487,9 @@ export class Browser {
|
|
|
1328
1487
|
async pages() {
|
|
1329
1488
|
if (this.bidiSession?.isConnected()) {
|
|
1330
1489
|
const conn = this.bidiSession.getConnection();
|
|
1331
|
-
const tree = await conn.send('browsingContext.getTree', {
|
|
1490
|
+
const tree = await conn.send('browsingContext.getTree', {
|
|
1491
|
+
maxDepth: 0,
|
|
1492
|
+
});
|
|
1332
1493
|
return (tree.contexts ?? []).map((ctx) => new Page(this.driver, ctx.context, this.getDefaultTimeout, conn, this._wrapContext(ctx.userContext ?? 'default')));
|
|
1333
1494
|
}
|
|
1334
1495
|
// Classic fallback: use window handles
|
|
@@ -1365,16 +1526,11 @@ export class Browser {
|
|
|
1365
1526
|
}
|
|
1366
1527
|
return page;
|
|
1367
1528
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
* const popup = await browser.waitForPage(() => browser.click('#open-popup'));
|
|
1374
|
-
* await popup.waitForLoadState();
|
|
1375
|
-
* const title = await popup.title();
|
|
1376
|
-
*/
|
|
1377
|
-
async waitForPage(action, opts) {
|
|
1529
|
+
async waitForPage(actionOrMatcher, opts) {
|
|
1530
|
+
if (typeof actionOrMatcher !== 'function') {
|
|
1531
|
+
return this._waitForMatchingPage(actionOrMatcher, opts);
|
|
1532
|
+
}
|
|
1533
|
+
const action = actionOrMatcher;
|
|
1378
1534
|
const timeout = opts?.timeout ?? this.defaults.navigationTimeout;
|
|
1379
1535
|
if (this.bidiSession?.isConnected()) {
|
|
1380
1536
|
const conn = this.bidiSession.getConnection();
|
|
@@ -1407,15 +1563,60 @@ export class Browser {
|
|
|
1407
1563
|
const deadline = Date.now() + timeout;
|
|
1408
1564
|
while (Date.now() < deadline) {
|
|
1409
1565
|
const after = await this.driver.getWindowHandles();
|
|
1410
|
-
const newHandles = after.filter(h => !before.has(h));
|
|
1566
|
+
const newHandles = after.filter((h) => !before.has(h));
|
|
1411
1567
|
if (newHandles.length > 0) {
|
|
1412
1568
|
const handle = newHandles[0];
|
|
1413
1569
|
return new Page(this.driver, handle, this.getDefaultTimeout);
|
|
1414
1570
|
}
|
|
1415
|
-
await new Promise(r => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
1571
|
+
await new Promise((r) => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
1416
1572
|
}
|
|
1417
1573
|
throw new Error(`waitForPage() timed out after ${timeout}ms — no new tab or popup appeared`);
|
|
1418
1574
|
}
|
|
1575
|
+
/** Poll open top-level pages until one matches `matcher`; see {@link waitForPage}. */
|
|
1576
|
+
async _waitForMatchingPage(matcher, opts) {
|
|
1577
|
+
if (matcher.url === undefined && matcher.title === undefined) {
|
|
1578
|
+
throw new Error('waitForPage(matcher): provide at least one of { url, title }.');
|
|
1579
|
+
}
|
|
1580
|
+
const timeout = opts?.timeout ?? this.defaults.navigationTimeout;
|
|
1581
|
+
const deadline = Date.now() + timeout;
|
|
1582
|
+
const seen = new Set();
|
|
1583
|
+
for (;;) {
|
|
1584
|
+
for (const page of await this.pages()) {
|
|
1585
|
+
try {
|
|
1586
|
+
if (await this._pageMatches(page, matcher, seen)) {
|
|
1587
|
+
// Classic: make the matched window current so browser.find(...) targets
|
|
1588
|
+
// it too (BiDi drives through the returned Page's own context).
|
|
1589
|
+
if (!this.bidiSession?.isConnected()) {
|
|
1590
|
+
await this.driver.switchToWindow(page.id()).catch(() => { });
|
|
1591
|
+
}
|
|
1592
|
+
return page;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
catch (err) {
|
|
1596
|
+
// A window that closed mid-check (e.g. the splash) — skip it, keep looking.
|
|
1597
|
+
if (!isNoSuchWindowError(err))
|
|
1598
|
+
throw err;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
if (Date.now() >= deadline) {
|
|
1602
|
+
const label = [
|
|
1603
|
+
matcher.title !== undefined ? `title ${String(matcher.title)}` : undefined,
|
|
1604
|
+
matcher.url !== undefined ? `url ${String(matcher.url)}` : undefined,
|
|
1605
|
+
]
|
|
1606
|
+
.filter(Boolean)
|
|
1607
|
+
.join(' and ');
|
|
1608
|
+
throw new Error(`waitForPage(matcher) timed out after ${timeout}ms — no window matched ${label}. ` +
|
|
1609
|
+
`Saw: ${[...seen].join(' | ') || '(no readable windows)'}.`);
|
|
1610
|
+
}
|
|
1611
|
+
await new Promise((r) => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
async _pageMatches(page, matcher, seen) {
|
|
1615
|
+
const url = matcher.url !== undefined ? await page.url() : undefined;
|
|
1616
|
+
const title = matcher.title !== undefined ? await page.title() : undefined;
|
|
1617
|
+
seen.add([title !== undefined ? `"${title}"` : '', url ?? ''].filter(Boolean).join(' '));
|
|
1618
|
+
return matchPageFields({ url, title }, matcher);
|
|
1619
|
+
}
|
|
1419
1620
|
// ─── User contexts (isolated profiles, BiDi-only) ────────────────────────
|
|
1420
1621
|
/**
|
|
1421
1622
|
* Create a new isolated user context (an "incognito profile"). Each
|
|
@@ -1472,7 +1673,9 @@ export class Browser {
|
|
|
1472
1673
|
this._contextsById.set(id, ctx);
|
|
1473
1674
|
// Evict on close so long-running suites that create many contexts don't
|
|
1474
1675
|
// accumulate dead wrappers.
|
|
1475
|
-
ctx.on('close', () => {
|
|
1676
|
+
ctx.on('close', () => {
|
|
1677
|
+
this._contextsById.delete(id);
|
|
1678
|
+
});
|
|
1476
1679
|
return ctx;
|
|
1477
1680
|
}
|
|
1478
1681
|
async newContext(opts) {
|
|
@@ -1561,7 +1764,9 @@ export class Browser {
|
|
|
1561
1764
|
// would break that contract if a future refactor made the branch reachable.
|
|
1562
1765
|
await conn
|
|
1563
1766
|
.subscribe(['browsingContext.contextCreated', 'browsingContext.contextDestroyed'])
|
|
1564
|
-
.catch(() => {
|
|
1767
|
+
.catch(() => {
|
|
1768
|
+
/* already subscribed or unsupported; fallback sync will cover us */
|
|
1769
|
+
});
|
|
1565
1770
|
}
|
|
1566
1771
|
const onCreated = (params) => {
|
|
1567
1772
|
if (params.parent)
|
|
@@ -1596,7 +1801,9 @@ export class Browser {
|
|
|
1596
1801
|
}
|
|
1597
1802
|
async _refreshTopLevelContextCache(conn) {
|
|
1598
1803
|
const version = this._topLevelContextCacheVersion;
|
|
1599
|
-
const tree = await conn.send('browsingContext.getTree', {
|
|
1804
|
+
const tree = await conn.send('browsingContext.getTree', {
|
|
1805
|
+
maxDepth: 0,
|
|
1806
|
+
});
|
|
1600
1807
|
if (this._topLevelContextCacheVersion === version) {
|
|
1601
1808
|
this._topLevelContextUserContexts.clear();
|
|
1602
1809
|
}
|
|
@@ -1653,8 +1860,30 @@ export class Browser {
|
|
|
1653
1860
|
}
|
|
1654
1861
|
return new Page(this.driver, fallback, this.getDefaultTimeout, conn, this.defaultContext);
|
|
1655
1862
|
}
|
|
1656
|
-
// Classic mode:
|
|
1657
|
-
|
|
1863
|
+
// Classic mode: wrap the current window. If it was closed (e.g. a splash that
|
|
1864
|
+
// replaced itself), re-point to the sole remaining top-level window when there's
|
|
1865
|
+
// exactly one — unambiguous — and otherwise ask the caller to select explicitly,
|
|
1866
|
+
// rather than guessing among several or throwing an opaque "no such window".
|
|
1867
|
+
let handle;
|
|
1868
|
+
try {
|
|
1869
|
+
handle = await this.driver.getCurrentWindowHandle();
|
|
1870
|
+
}
|
|
1871
|
+
catch (err) {
|
|
1872
|
+
if (!isNoSuchWindowError(err))
|
|
1873
|
+
throw err;
|
|
1874
|
+
}
|
|
1875
|
+
const handles = await this.driver.getWindowHandles();
|
|
1876
|
+
if (handle === undefined || !handles.includes(handle)) {
|
|
1877
|
+
if (handles.length === 0) {
|
|
1878
|
+
throw new Error('activePage(): all windows are closed.');
|
|
1879
|
+
}
|
|
1880
|
+
if (handles.length > 1) {
|
|
1881
|
+
throw new Error('activePage(): the current window was closed and several windows are open. ' +
|
|
1882
|
+
'Select one with browser.waitForPage({ title } | { url }) or browser.pages().');
|
|
1883
|
+
}
|
|
1884
|
+
handle = handles[0];
|
|
1885
|
+
await this.driver.switchToWindow(handle).catch(() => { });
|
|
1886
|
+
}
|
|
1658
1887
|
return new Page(this.driver, handle, this.getDefaultTimeout);
|
|
1659
1888
|
}
|
|
1660
1889
|
async click(selector, opts) {
|
|
@@ -1807,19 +2036,27 @@ export class Browser {
|
|
|
1807
2036
|
static Key = Key;
|
|
1808
2037
|
async waitForVisible(selector, opts) {
|
|
1809
2038
|
const by = typeof selector === 'string' ? By.css(selector) : selector;
|
|
1810
|
-
await this.driver.wait(until.elementIsVisible(by), {
|
|
2039
|
+
await this.driver.wait(until.elementIsVisible(by), {
|
|
2040
|
+
timeout: opts?.timeout ?? this.defaults.timeout,
|
|
2041
|
+
});
|
|
1811
2042
|
}
|
|
1812
2043
|
async waitForHidden(selector, opts) {
|
|
1813
2044
|
const by = typeof selector === 'string' ? By.css(selector) : selector;
|
|
1814
|
-
await this.driver.wait(until.elementIsNotVisible(by), {
|
|
2045
|
+
await this.driver.wait(until.elementIsNotVisible(by), {
|
|
2046
|
+
timeout: opts?.timeout ?? this.defaults.timeout,
|
|
2047
|
+
});
|
|
1815
2048
|
}
|
|
1816
2049
|
async waitForAttached(selector, opts) {
|
|
1817
2050
|
const by = typeof selector === 'string' ? By.css(selector) : selector;
|
|
1818
|
-
await this.driver.wait(until.elementExists(by), {
|
|
2051
|
+
await this.driver.wait(until.elementExists(by), {
|
|
2052
|
+
timeout: opts?.timeout ?? this.defaults.timeout,
|
|
2053
|
+
});
|
|
1819
2054
|
}
|
|
1820
2055
|
async waitForDetached(selector, opts) {
|
|
1821
2056
|
const by = typeof selector === 'string' ? By.css(selector) : selector;
|
|
1822
|
-
await this.driver.wait(until.elementNotExists(by), {
|
|
2057
|
+
await this.driver.wait(until.elementNotExists(by), {
|
|
2058
|
+
timeout: opts?.timeout ?? this.defaults.timeout,
|
|
2059
|
+
});
|
|
1823
2060
|
}
|
|
1824
2061
|
/**
|
|
1825
2062
|
* Wait for an element to reach the given state.
|
|
@@ -1833,10 +2070,14 @@ export class Browser {
|
|
|
1833
2070
|
*/
|
|
1834
2071
|
async waitFor(selector, opts) {
|
|
1835
2072
|
switch (opts.state) {
|
|
1836
|
-
case 'visible':
|
|
1837
|
-
|
|
1838
|
-
case '
|
|
1839
|
-
|
|
2073
|
+
case 'visible':
|
|
2074
|
+
return this.waitForVisible(selector, opts);
|
|
2075
|
+
case 'hidden':
|
|
2076
|
+
return this.waitForHidden(selector, opts);
|
|
2077
|
+
case 'attached':
|
|
2078
|
+
return this.waitForAttached(selector, opts);
|
|
2079
|
+
case 'detached':
|
|
2080
|
+
return this.waitForDetached(selector, opts);
|
|
1840
2081
|
}
|
|
1841
2082
|
}
|
|
1842
2083
|
actions() {
|