craftdriver 1.3.0 → 1.5.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 +17 -3
- package/README.md +4 -1
- package/dist/cli/dispatcher.js +1 -1
- package/dist/cli/dispatcher.js.map +1 -1
- package/dist/cli/parseArgs.d.ts +2 -2
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/bidi/storage.d.ts +13 -0
- package/dist/lib/bidi/storage.d.ts.map +1 -1
- package/dist/lib/bidi/storage.js +81 -32
- package/dist/lib/bidi/storage.js.map +1 -1
- package/dist/lib/browser.d.ts +173 -21
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +519 -210
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/browserContext.d.ts +2 -2
- package/dist/lib/browserContext.d.ts.map +1 -1
- package/dist/lib/browserContext.js +1 -1
- package/dist/lib/builder.d.ts +4 -1
- package/dist/lib/builder.d.ts.map +1 -1
- package/dist/lib/builder.js +20 -2
- package/dist/lib/builder.js.map +1 -1
- package/dist/lib/capabilities.d.ts +45 -0
- package/dist/lib/capabilities.d.ts.map +1 -0
- package/dist/lib/capabilities.js +103 -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/driver.d.ts +16 -1
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +64 -0
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/driverManager.d.ts +120 -0
- package/dist/lib/driverManager.d.ts.map +1 -1
- package/dist/lib/driverManager.js +289 -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 +174 -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/safari.d.ts +90 -0
- package/dist/lib/safari.d.ts.map +1 -0
- package/dist/lib/safari.js +136 -0
- package/dist/lib/safari.js.map +1 -0
- 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/dist/lib/tracing.d.ts +1 -1
- package/dist/lib/tracing.d.ts.map +1 -1
- package/dist/lib/tracing.js.map +1 -1
- package/dist/lib/types.d.ts +41 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/vibiumTrace.d.ts +1 -1
- package/dist/lib/vibiumTrace.d.ts.map +1 -1
- package/dist/lib/webelement.d.ts.map +1 -1
- package/dist/lib/webelement.js +118 -2
- package/dist/lib/webelement.js.map +1 -1
- package/docs/api-reference.md +39 -2
- package/docs/browser-api.md +7 -1
- package/docs/driver-configuration.md +54 -0
- package/docs/electron.md +419 -0
- package/docs/error-codes.md +5 -0
- package/docs/getting-started.md +30 -10
- package/docs/index.md +5 -1
- package/docs/public/examples/displayed.html +86 -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/docs/safari.md +118 -0
- package/docs/standards.md +13 -0
- package/docs/why-craftdriver.md +1 -1
- package/docs/zero-config-drivers.md +12 -0
- package/package.json +7 -1
- package/skills/craftdriver/SKILL.md +4 -1
- package/skills/craftdriver/cheatsheet.md +4 -1
package/dist/lib/browser.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
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';
|
|
8
|
+
import { SafariService } from './safari.js';
|
|
4
9
|
import { resolveBrowserBinaryPath } from './driverManager.js';
|
|
10
|
+
import { buildLaunchCapabilities } from './capabilities.js';
|
|
11
|
+
import { resolveLaunchTarget } from './launchTarget.js';
|
|
5
12
|
import { By } from './by.js';
|
|
6
13
|
import { until } from './wait.js';
|
|
7
14
|
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 +25,7 @@ import { Mouse } from './mouse.js';
|
|
|
18
25
|
import { ActionsBuilder } from './actions.js';
|
|
19
26
|
import { BiDiSession, SessionStateManager, } from './bidi/index.js';
|
|
20
27
|
import { Frame } from './frame.js';
|
|
21
|
-
import { Page } from './page.js';
|
|
28
|
+
import { Page, isNoSuchWindowError } from './page.js';
|
|
22
29
|
import { bidiWaitFor } from './loadState.js';
|
|
23
30
|
import { BrowserContext } from './browserContext.js';
|
|
24
31
|
import { Tracer } from './tracing.js';
|
|
@@ -63,6 +70,75 @@ export const devices = {
|
|
|
63
70
|
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
71
|
},
|
|
65
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a `goog:chromeOptions.mobileEmulation` payload from the public
|
|
75
|
+
* `MobileEmulation | DeviceName` option — a built-in device name maps to
|
|
76
|
+
* Chrome's `deviceName`, otherwise a custom `deviceMetrics`/`userAgent` config
|
|
77
|
+
* is built. Kept next to `devices`; `buildLaunchCapabilities` takes the result.
|
|
78
|
+
*/
|
|
79
|
+
function resolveMobileEmulationConfig(mobileEmulation) {
|
|
80
|
+
const emulation = typeof mobileEmulation === 'string' ? devices[mobileEmulation] : mobileEmulation;
|
|
81
|
+
if ('deviceName' in emulation && emulation.deviceName) {
|
|
82
|
+
// Use Chrome's built-in device name
|
|
83
|
+
return { deviceName: emulation.deviceName };
|
|
84
|
+
}
|
|
85
|
+
// Build custom mobile emulation config
|
|
86
|
+
const mobileConfig = {};
|
|
87
|
+
if (emulation.deviceMetrics) {
|
|
88
|
+
mobileConfig.deviceMetrics = {
|
|
89
|
+
width: emulation.deviceMetrics.width,
|
|
90
|
+
height: emulation.deviceMetrics.height,
|
|
91
|
+
pixelRatio: emulation.deviceMetrics.pixelRatio,
|
|
92
|
+
mobile: emulation.deviceMetrics.mobile ?? true,
|
|
93
|
+
touch: emulation.deviceMetrics.touch ?? true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (emulation.userAgent) {
|
|
97
|
+
mobileConfig.userAgent = emulation.userAgent;
|
|
98
|
+
}
|
|
99
|
+
return mobileConfig;
|
|
100
|
+
}
|
|
101
|
+
async function resolveElectronAppBinaryPath(appBinaryPath) {
|
|
102
|
+
const resolved = path.resolve(appBinaryPath);
|
|
103
|
+
let stat;
|
|
104
|
+
try {
|
|
105
|
+
stat = await fs.stat(resolved);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
const code = error.code;
|
|
109
|
+
if (code === 'ENOENT') {
|
|
110
|
+
throw new Error(`electron.appBinaryPath "${appBinaryPath}" does not exist.`);
|
|
111
|
+
}
|
|
112
|
+
if (code === 'EACCES') {
|
|
113
|
+
throw new Error(`electron.appBinaryPath "${appBinaryPath}" is not accessible.`);
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
if (!stat.isFile()) {
|
|
118
|
+
throw new Error(`electron.appBinaryPath "${appBinaryPath}" is not a file.`);
|
|
119
|
+
}
|
|
120
|
+
if (process.platform !== 'win32') {
|
|
121
|
+
try {
|
|
122
|
+
await fs.access(resolved, fsSync.constants.X_OK);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw new Error(`electron.appBinaryPath "${appBinaryPath}" is not executable.`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return resolved;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Pure predicate behind {@link Browser.waitForPage}'s matcher form: do a page's
|
|
132
|
+
* `url`/`title` satisfy the matcher? Only the fields named in `matcher` are checked,
|
|
133
|
+
* and every named field must match (string = substring, RegExp = test). Exported for
|
|
134
|
+
* unit testing.
|
|
135
|
+
*/
|
|
136
|
+
export function matchPageFields(fields, matcher) {
|
|
137
|
+
const hit = (value, pattern) => pattern === undefined ||
|
|
138
|
+
(value !== undefined &&
|
|
139
|
+
(typeof pattern === 'string' ? value.includes(pattern) : pattern.test(value)));
|
|
140
|
+
return hit(fields.url, matcher.url) && hit(fields.title, matcher.title);
|
|
141
|
+
}
|
|
66
142
|
// ---------------------------------------------------------------------------
|
|
67
143
|
// BiDi value helpers — used only by Browser.evaluate()
|
|
68
144
|
// ---------------------------------------------------------------------------
|
|
@@ -93,7 +169,10 @@ function serializeLocalValue(v) {
|
|
|
93
169
|
if (typeof v === 'object') {
|
|
94
170
|
return {
|
|
95
171
|
type: 'object',
|
|
96
|
-
value: Object.entries(v).map(([k, val]) => [
|
|
172
|
+
value: Object.entries(v).map(([k, val]) => [
|
|
173
|
+
k,
|
|
174
|
+
serializeLocalValue(val),
|
|
175
|
+
]),
|
|
97
176
|
};
|
|
98
177
|
}
|
|
99
178
|
throw new CraftdriverError(ErrorCode.EVAL_BAD_ARG, `evaluate() argument of type "${typeof v}" is not JSON-serializable. ` +
|
|
@@ -101,11 +180,16 @@ function serializeLocalValue(v) {
|
|
|
101
180
|
}
|
|
102
181
|
function unwrapRemoteValue(v) {
|
|
103
182
|
switch (v.type) {
|
|
104
|
-
case 'undefined':
|
|
105
|
-
|
|
106
|
-
case '
|
|
107
|
-
|
|
108
|
-
case '
|
|
183
|
+
case 'undefined':
|
|
184
|
+
return undefined;
|
|
185
|
+
case 'null':
|
|
186
|
+
return null;
|
|
187
|
+
case 'string':
|
|
188
|
+
return v.value;
|
|
189
|
+
case 'boolean':
|
|
190
|
+
return v.value;
|
|
191
|
+
case 'bigint':
|
|
192
|
+
return BigInt(v.value);
|
|
109
193
|
case 'number': {
|
|
110
194
|
if (v.value === 'NaN')
|
|
111
195
|
return NaN;
|
|
@@ -117,13 +201,15 @@ function unwrapRemoteValue(v) {
|
|
|
117
201
|
return -0;
|
|
118
202
|
return v.value;
|
|
119
203
|
}
|
|
120
|
-
case 'array':
|
|
204
|
+
case 'array':
|
|
205
|
+
return (v.value ?? []).map(unwrapRemoteValue);
|
|
121
206
|
case 'object':
|
|
122
207
|
return Object.fromEntries((v.value ?? []).map(([k, val]) => [
|
|
123
208
|
typeof k === 'string' ? k : String(unwrapRemoteValue(k)),
|
|
124
209
|
unwrapRemoteValue(val),
|
|
125
210
|
]));
|
|
126
|
-
case 'date':
|
|
211
|
+
case 'date':
|
|
212
|
+
return new Date(v.value);
|
|
127
213
|
case 'function':
|
|
128
214
|
throw new CraftdriverError(ErrorCode.EVAL_BAD_ARG, 'evaluate() returned a function, which is not JSON-serializable. ' +
|
|
129
215
|
'Return a primitive, array, or plain object instead.', { detail: { returnedType: 'function' } });
|
|
@@ -190,11 +276,19 @@ export class Browser {
|
|
|
190
276
|
_browserInitScriptIds = new Set();
|
|
191
277
|
_downloadsDir;
|
|
192
278
|
_driverService;
|
|
279
|
+
/** Main-process inspector endpoint, set when launched with `electron.mainProcess`. */
|
|
280
|
+
_electronInspect;
|
|
281
|
+
/** The launched Electron app's executable path (for deep-link routing on Windows). */
|
|
282
|
+
_electronAppBinaryPath;
|
|
283
|
+
_electron;
|
|
193
284
|
_browserName = 'chrome';
|
|
194
285
|
/** Active emulation overrides, re-applied to new top-level contexts. */
|
|
195
286
|
_emulation = {};
|
|
196
287
|
/** Mutable browser-level defaults. Use setDefaultTimeout() / setDefaultNavigationTimeout() to change. */
|
|
197
|
-
defaults = {
|
|
288
|
+
defaults = {
|
|
289
|
+
timeout: DEFAULT_ELEMENT_TIMEOUT_MS,
|
|
290
|
+
navigationTimeout: DEFAULT_NAVIGATION_TIMEOUT_MS,
|
|
291
|
+
};
|
|
198
292
|
constructor(driver) {
|
|
199
293
|
this.driver = driver;
|
|
200
294
|
this.keyboard = new Keyboard(this.driver);
|
|
@@ -219,117 +313,158 @@ export class Browser {
|
|
|
219
313
|
hasBrowserInitScripts = () => this._browserInitScriptIds.size > 0;
|
|
220
314
|
hasDefaultNavigationInitScripts = () => this._defaultContext?._hasInitScriptsForNavigation() ?? this.hasBrowserInitScripts();
|
|
221
315
|
static async launch(options = {}) {
|
|
222
|
-
|
|
223
|
-
|
|
316
|
+
// Normalize first: callers may be JavaScript or JSON-driven, so reject
|
|
317
|
+
// malformed/mixed targets before touching disk or starting a driver.
|
|
318
|
+
const target = resolveLaunchTarget(options);
|
|
319
|
+
const isElectron = target.kind === 'electron';
|
|
320
|
+
const name = target.browserName;
|
|
224
321
|
const isChromeFamily = name === 'chrome' || name === 'chromium';
|
|
225
|
-
if (options.mobileEmulation && !isChromeFamily) {
|
|
322
|
+
if (target.kind === 'browser' && options.mobileEmulation && !isChromeFamily) {
|
|
226
323
|
throw new Error(`mobileEmulation is only supported on Chrome/Chromium (got "${name}"). ` +
|
|
227
324
|
`Firefox does not expose a device-emulation API equivalent to goog:chromeOptions.mobileEmulation.`);
|
|
228
325
|
}
|
|
229
|
-
|
|
326
|
+
const bidiRequested = target.bidiRequested;
|
|
327
|
+
// Handle headless mode via env var (never applied to Electron — see capabilities.ts).
|
|
230
328
|
const headlessEnv = process.env.HEADLESS;
|
|
231
329
|
const isHeadless = headlessEnv === 'true' || headlessEnv === '1';
|
|
330
|
+
// Resolve Electron before creating session directories. A malformed or
|
|
331
|
+
// missing executable must never degrade into a regular Chrome launch.
|
|
332
|
+
const electronBinary = target.kind === 'electron'
|
|
333
|
+
? await resolveElectronAppBinaryPath(target.appBinaryPath)
|
|
334
|
+
: undefined;
|
|
335
|
+
if (options.electronService !== undefined &&
|
|
336
|
+
!(options.electronService instanceof ElectronService)) {
|
|
337
|
+
throw new Error('electronService must be an ElectronService instance.');
|
|
338
|
+
}
|
|
339
|
+
if (target.kind === 'electron') {
|
|
340
|
+
writeElectronDebug({
|
|
341
|
+
appPath: electronBinary,
|
|
342
|
+
protocol: bidiRequested ? 'bidi' : 'classic',
|
|
343
|
+
platform: `${os.platform()}-${os.arch()}`,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
232
346
|
// Set up downloads directory
|
|
233
|
-
const downloadsDir = options.downloadsDir ??
|
|
347
|
+
const downloadsDir = options.downloadsDir ??
|
|
348
|
+
path.join(os.tmpdir(), 'craftdriver-downloads', `session-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
234
349
|
await fs.mkdir(downloadsDir, { recursive: true });
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
// vars in
|
|
239
|
-
// conventions other tools may
|
|
240
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
}
|
|
350
|
+
// The binary that becomes goog:chromeOptions.binary / moz:firefoxOptions.binary.
|
|
351
|
+
// For Electron it's the app executable (explicit, required). For browsers
|
|
352
|
+
// it's the optional custom-binary resolution chain — but see
|
|
353
|
+
// resolveBrowserBinaryPath's doc: some of the env vars in that chain aren't
|
|
354
|
+
// craftdriver-opt-in, they're ambient conventions other tools may have set.
|
|
355
|
+
// Safari has no custom-binary concept: browserPath/args are rejected
|
|
356
|
+
// upstream in resolveLaunchTarget(), so target.browserPath is
|
|
357
|
+
// always undefined here. Skip resolveBrowserBinaryPath() for Safari
|
|
358
|
+
// entirely rather than calling it with 'firefox' as a stand-in — it would
|
|
359
|
+
// still probe FIREFOX_BIN/SE_FIREFOX_PATH-style env vars that have
|
|
360
|
+
// nothing to do with Safari, which is misleading even though it happens
|
|
361
|
+
// to return undefined/harmless today.
|
|
362
|
+
const browserBinary = target.kind === 'electron'
|
|
363
|
+
? electronBinary
|
|
364
|
+
: name === 'safari'
|
|
365
|
+
? undefined
|
|
366
|
+
: resolveBrowserBinaryPath(isChromeFamily ? 'chrome' : 'firefox', target.browserPath);
|
|
367
|
+
// Main-process access (opt-in): launch the Electron app with a Node inspector
|
|
368
|
+
// on a free local port. chromedriver forwards this arg to the app, enabling
|
|
369
|
+
// its main-process inspector; browser.electron connects below for log capture
|
|
370
|
+
// and retries on first executeMain() if that eager connection was too early.
|
|
371
|
+
let electronInspect;
|
|
372
|
+
let launchArgs = target.args;
|
|
373
|
+
if (target.kind === 'electron' && target.mainProcess) {
|
|
374
|
+
const host = '127.0.0.1';
|
|
375
|
+
const port = await findFreePort();
|
|
376
|
+
electronInspect = { host, port };
|
|
377
|
+
launchArgs = [...(target.args ?? []), `--inspect=${host}:${port}`];
|
|
378
|
+
}
|
|
379
|
+
const caps = buildLaunchCapabilities({
|
|
380
|
+
browserName: name,
|
|
381
|
+
isElectron,
|
|
382
|
+
isHeadless,
|
|
383
|
+
bidiRequested,
|
|
384
|
+
browserBinary,
|
|
385
|
+
downloadsDir,
|
|
386
|
+
args: launchArgs,
|
|
387
|
+
mobileEmulation: target.kind === 'browser' && isChromeFamily && options.mobileEmulation
|
|
388
|
+
? resolveMobileEmulationConfig(options.mobileEmulation)
|
|
389
|
+
: undefined,
|
|
390
|
+
});
|
|
311
391
|
const builder = new Builder().forBrowser(name);
|
|
312
392
|
let driverService;
|
|
313
|
-
if (
|
|
393
|
+
if (target.kind === 'electron') {
|
|
394
|
+
driverService =
|
|
395
|
+
options.electronService ??
|
|
396
|
+
new ElectronService({
|
|
397
|
+
chromedriverPath: target.chromedriverPath,
|
|
398
|
+
version: target.version,
|
|
399
|
+
// Lets the driver resolver read the Electron version from a packaged app's
|
|
400
|
+
// bundle when no explicit version/driver is given (production apps).
|
|
401
|
+
appBinaryPath: electronBinary,
|
|
402
|
+
});
|
|
403
|
+
builder.setChromeService(driverService);
|
|
404
|
+
}
|
|
405
|
+
else if (isChromeFamily) {
|
|
314
406
|
// Only thread browserPath into a default ChromeService — a
|
|
315
407
|
// caller-supplied one is expected to already pin what it needs (same
|
|
316
408
|
// reasoning as explicit config always winning over auto-detection).
|
|
317
|
-
driverService = options.chromeService ?? new ChromeService({ browserPath });
|
|
409
|
+
driverService = options.chromeService ?? new ChromeService({ browserPath: browserBinary });
|
|
318
410
|
builder.setChromeService(driverService);
|
|
319
411
|
}
|
|
412
|
+
else if (name === 'safari') {
|
|
413
|
+
// No browserPath threading (Safari has none — see the browserBinary
|
|
414
|
+
// resolution above) and no headless/args to pass through; SafariService
|
|
415
|
+
// resolves its own driver binary (never auto-downloaded).
|
|
416
|
+
driverService = options.safariService ?? new SafariService();
|
|
417
|
+
builder.setSafariService(driverService);
|
|
418
|
+
}
|
|
320
419
|
else {
|
|
321
420
|
driverService = options.firefoxService ?? new FirefoxService();
|
|
322
421
|
builder.setFirefoxService(driverService);
|
|
323
422
|
}
|
|
324
423
|
builder.withCapabilities(caps);
|
|
325
|
-
|
|
424
|
+
let driver;
|
|
425
|
+
if (target.kind === 'electron') {
|
|
426
|
+
try {
|
|
427
|
+
driver = await builder.build();
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
// Turn an opaque "Chrome instance exited" into a diagnosed, actionable
|
|
431
|
+
// error (macOS signing/Gatekeeper, Linux sandbox) with the chromedriver
|
|
432
|
+
// output tail attached. Non-app-exit failures pass through unchanged.
|
|
433
|
+
throw diagnoseElectronLaunchFailure(err, {
|
|
434
|
+
appBinaryPath: electronBinary,
|
|
435
|
+
args: target.args,
|
|
436
|
+
driverOutputTail: driverService.getOutputTail?.(),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
driver = await builder.build();
|
|
442
|
+
}
|
|
326
443
|
const browser = new Browser(driver);
|
|
327
444
|
browser._downloadsDir = downloadsDir;
|
|
328
445
|
browser._driverService = driverService;
|
|
329
446
|
browser._browserName = name;
|
|
447
|
+
browser._electronInspect = electronInspect;
|
|
448
|
+
browser._electronAppBinaryPath = electronBinary;
|
|
449
|
+
// With main-process access enabled, open the inspector bridge now so
|
|
450
|
+
// main-process console/error capture (browser.electron.mainLogs) starts at
|
|
451
|
+
// launch, mirroring always-on renderer log capture. Best-effort: a disabled
|
|
452
|
+
// EnableNodeCliInspectArguments fuse or an unreachable inspector must never
|
|
453
|
+
// fail renderer automation — executeMain still surfaces the actionable error
|
|
454
|
+
// on first use.
|
|
455
|
+
if (electronInspect) {
|
|
456
|
+
const remote = new ElectronRemote(electronInspect, { appBinaryPath: electronBinary });
|
|
457
|
+
browser._electron = remote;
|
|
458
|
+
await remote.connect().catch((err) => {
|
|
459
|
+
writeElectronDebug({
|
|
460
|
+
event: 'main-bridge-eager-connect-failed',
|
|
461
|
+
error: err instanceof Error ? err.message : String(err),
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
}
|
|
330
465
|
// Initialize BiDi session if WebSocket URL available
|
|
331
466
|
const wsUrl = driver.__wsUrl;
|
|
332
|
-
if (wsUrl &&
|
|
467
|
+
if (wsUrl && bidiRequested) {
|
|
333
468
|
await browser.initBiDi(wsUrl);
|
|
334
469
|
}
|
|
335
470
|
// Load session state if provided
|
|
@@ -380,6 +515,26 @@ export class Browser {
|
|
|
380
515
|
isBiDiEnabled() {
|
|
381
516
|
return this.bidiSession?.isConnected() ?? false;
|
|
382
517
|
}
|
|
518
|
+
/**
|
|
519
|
+
* Guard for the BiDi-only surface below. On Safari (which never
|
|
520
|
+
* negotiates BiDi) this throws `CraftdriverError`/`UNSUPPORTED` with a
|
|
521
|
+
* stable `{ browserName, feature }` detail so callers can branch on `code`
|
|
522
|
+
* instead of parsing prose. On every other browser the behavior is
|
|
523
|
+
* unchanged: the exact plain-`Error` message passed by the call site.
|
|
524
|
+
*
|
|
525
|
+
* Deliberately scoped to Safari's guards only — the pre-existing
|
|
526
|
+
* plain-`Error` guards elsewhere are a separate follow-up, not silently
|
|
527
|
+
* folded into this helper.
|
|
528
|
+
*/
|
|
529
|
+
requireBiDi(feature, chromeMessage) {
|
|
530
|
+
if (this.bidiSession?.isConnected())
|
|
531
|
+
return;
|
|
532
|
+
if (this._browserName === 'safari') {
|
|
533
|
+
throw new CraftdriverError(ErrorCode.UNSUPPORTED, `${feature} is not supported on Safari (no WebDriver BiDi). ` +
|
|
534
|
+
'Apple does not document a WebDriver BiDi endpoint for Safari.', { detail: { browserName: 'safari', feature } });
|
|
535
|
+
}
|
|
536
|
+
throw new Error(chromeMessage);
|
|
537
|
+
}
|
|
383
538
|
// === BiDi Feature Accessors ===
|
|
384
539
|
/**
|
|
385
540
|
* Network interception API (BiDi)
|
|
@@ -387,10 +542,8 @@ export class Browser {
|
|
|
387
542
|
*/
|
|
388
543
|
get network() {
|
|
389
544
|
if (!this._network) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
'BiDi negotiation may have failed at launch — check browser logs for WebSocket errors.');
|
|
393
|
-
}
|
|
545
|
+
this.requireBiDi('network', 'Network interception requires BiDi. ' +
|
|
546
|
+
'BiDi negotiation may have failed at launch — check browser logs for WebSocket errors.');
|
|
394
547
|
this._network = this.bidiSession.network;
|
|
395
548
|
}
|
|
396
549
|
return this._network;
|
|
@@ -400,10 +553,8 @@ export class Browser {
|
|
|
400
553
|
*/
|
|
401
554
|
get logs() {
|
|
402
555
|
if (!this._logs) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
'BiDi negotiation may have failed at launch — check browser logs for WebSocket errors.');
|
|
406
|
-
}
|
|
556
|
+
this.requireBiDi('logs', 'Log monitoring requires BiDi. ' +
|
|
557
|
+
'BiDi negotiation may have failed at launch — check browser logs for WebSocket errors.');
|
|
407
558
|
this._logs = this.bidiSession.logs;
|
|
408
559
|
}
|
|
409
560
|
return this._logs;
|
|
@@ -448,6 +599,26 @@ export class Browser {
|
|
|
448
599
|
}
|
|
449
600
|
return this._a11y;
|
|
450
601
|
}
|
|
602
|
+
/**
|
|
603
|
+
* Electron **main-process** access. `browser.electron.executeMain(fn, ...args)`
|
|
604
|
+
* runs `fn(electron, ...args)` in the app's main process (full `electron`
|
|
605
|
+
* module), complementing the renderer automation on `Browser` itself.
|
|
606
|
+
*
|
|
607
|
+
* Requires launching with `electron: { mainProcess: true }`; otherwise
|
|
608
|
+
* `executeMain` throws `ELECTRON_MAIN_UNAVAILABLE`.
|
|
609
|
+
*
|
|
610
|
+
* ```ts
|
|
611
|
+
* const version = await browser.electron.executeMain((electron) => electron.app.getVersion());
|
|
612
|
+
* ```
|
|
613
|
+
*/
|
|
614
|
+
get electron() {
|
|
615
|
+
if (!this._electron) {
|
|
616
|
+
this._electron = new ElectronRemote(this._electronInspect, {
|
|
617
|
+
appBinaryPath: this._electronAppBinaryPath,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
return this._electron;
|
|
621
|
+
}
|
|
451
622
|
/**
|
|
452
623
|
* Save current session state (cookies + localStorage) to file
|
|
453
624
|
*/
|
|
@@ -600,11 +771,9 @@ export class Browser {
|
|
|
600
771
|
* await browser.grantPermissions(['clipboard-read'], { origin: 'https://example.com' });
|
|
601
772
|
*/
|
|
602
773
|
async grantPermissions(permissions, opts) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
'which has no Classic-WebDriver equivalent.');
|
|
607
|
-
}
|
|
774
|
+
this.requireBiDi('grantPermissions()', 'grantPermissions() requires BiDi (enableBiDi: true). ' +
|
|
775
|
+
'Permission overrides use the W3C BiDi `permissions.setPermission` command, ' +
|
|
776
|
+
'which has no Classic-WebDriver equivalent.');
|
|
608
777
|
if (!Array.isArray(permissions) || permissions.length === 0) {
|
|
609
778
|
throw new Error('grantPermissions: pass a non-empty array of permission names.');
|
|
610
779
|
}
|
|
@@ -643,11 +812,9 @@ export class Browser {
|
|
|
643
812
|
* await browser.setGeolocation(null); // clear
|
|
644
813
|
*/
|
|
645
814
|
async setGeolocation(coords) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
'command, which has no Classic-WebDriver equivalent.');
|
|
650
|
-
}
|
|
815
|
+
this.requireBiDi('setGeolocation()', 'setGeolocation() requires BiDi (enableBiDi: true). ' +
|
|
816
|
+
'Geolocation overrides use the W3C BiDi `emulation.setGeolocationOverride` ' +
|
|
817
|
+
'command, which has no Classic-WebDriver equivalent.');
|
|
651
818
|
const conn = this.bidiSession.getConnection();
|
|
652
819
|
// Apply across all top-level contexts in the default user context.
|
|
653
820
|
const tree = await conn.send('browsingContext.getTree', { maxDepth: 0 });
|
|
@@ -660,8 +827,10 @@ export class Browser {
|
|
|
660
827
|
else {
|
|
661
828
|
if (typeof coords.latitude !== 'number' ||
|
|
662
829
|
typeof coords.longitude !== 'number' ||
|
|
663
|
-
coords.latitude < -90 ||
|
|
664
|
-
coords.
|
|
830
|
+
coords.latitude < -90 ||
|
|
831
|
+
coords.latitude > 90 ||
|
|
832
|
+
coords.longitude < -180 ||
|
|
833
|
+
coords.longitude > 180) {
|
|
665
834
|
throw new Error(`setGeolocation: invalid coordinates ${JSON.stringify(coords)}. ` +
|
|
666
835
|
'latitude must be in [-90, 90] and longitude in [-180, 180].');
|
|
667
836
|
}
|
|
@@ -711,12 +880,10 @@ export class Browser {
|
|
|
711
880
|
async emulate(options) {
|
|
712
881
|
if (!options || Object.keys(options).length === 0)
|
|
713
882
|
return;
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
'which have no Classic-WebDriver equivalent.');
|
|
719
|
-
}
|
|
883
|
+
this.requireBiDi('emulate()', 'emulate() requires BiDi (enableBiDi: true). ' +
|
|
884
|
+
'Emulation overrides use the W3C BiDi `emulation.*` commands ' +
|
|
885
|
+
'(and the BiDi+CDP bridge for media features and offline), ' +
|
|
886
|
+
'which have no Classic-WebDriver equivalent.');
|
|
720
887
|
// Validate Chromium-only fields up front so we fail before mutating state.
|
|
721
888
|
const chromiumOnly = ['colorScheme', 'reducedMotion', 'forcedColors', 'offline'];
|
|
722
889
|
const isFirefox = this._browserName === 'firefox';
|
|
@@ -836,7 +1003,7 @@ export class Browser {
|
|
|
836
1003
|
}
|
|
837
1004
|
// Classic fallback: best-effort 500ms settle
|
|
838
1005
|
else {
|
|
839
|
-
await new Promise(r => setTimeout(r, NETWORK_IDLE_SETTLE_MS));
|
|
1006
|
+
await new Promise((r) => setTimeout(r, NETWORK_IDLE_SETTLE_MS));
|
|
840
1007
|
}
|
|
841
1008
|
return;
|
|
842
1009
|
}
|
|
@@ -881,7 +1048,7 @@ export class Browser {
|
|
|
881
1048
|
const readyState = await this.driver.executeScript('return document.readyState', []);
|
|
882
1049
|
if (readyState === 'complete' || (target === 'interactive' && readyState === 'interactive'))
|
|
883
1050
|
return;
|
|
884
|
-
await new Promise(r => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
1051
|
+
await new Promise((r) => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
885
1052
|
}
|
|
886
1053
|
throw new CraftdriverError(ErrorCode.TIMEOUT_WAITING_LOAD, `waitForLoadState('${state}') timed out after ${timeout}ms`, { detail: { state, timeout } });
|
|
887
1054
|
}
|
|
@@ -901,6 +1068,10 @@ export class Browser {
|
|
|
901
1068
|
// Whether this session had a live BiDi WebSocket. Only the Firefox+BiDi
|
|
902
1069
|
// combination needs the port-release pause below; capture it before close().
|
|
903
1070
|
const bidiWasActive = !!this.bidiSession;
|
|
1071
|
+
// Close the Electron main-process inspector bridge, if one was opened.
|
|
1072
|
+
if (this._electron) {
|
|
1073
|
+
await this._electron.close().catch(() => { });
|
|
1074
|
+
}
|
|
904
1075
|
// Close BiDi connection first
|
|
905
1076
|
if (this.bidiSession) {
|
|
906
1077
|
await this.bidiSession.close().catch(() => { });
|
|
@@ -952,7 +1123,7 @@ export class Browser {
|
|
|
952
1123
|
async startTrace(opts) {
|
|
953
1124
|
if (!this.bidiSession?.isConnected()) {
|
|
954
1125
|
throw new CraftdriverError(ErrorCode.UNSUPPORTED, 'startTrace() requires BiDi (enableBiDi: true). ' +
|
|
955
|
-
'Tracing relies on BiDi events; Classic WebDriver does not expose them.', { detail: { feature: 'startTrace' } });
|
|
1126
|
+
'Tracing relies on BiDi events; Classic WebDriver does not expose them.', { detail: { browserName: this._browserName, feature: 'startTrace' } });
|
|
956
1127
|
}
|
|
957
1128
|
if (!this._tracer) {
|
|
958
1129
|
this._tracer = new Tracer(this, this.bidiSession.getConnection(), this._browserName);
|
|
@@ -1015,7 +1186,7 @@ export class Browser {
|
|
|
1015
1186
|
catch (err) {
|
|
1016
1187
|
if (attempt < EVAL_REALM_RETRY_ATTEMPTS &&
|
|
1017
1188
|
String(err?.message).includes('execution contexts cleared')) {
|
|
1018
|
-
await new Promise(r => setTimeout(r, EVAL_REALM_RETRY_DELAY_MS));
|
|
1189
|
+
await new Promise((r) => setTimeout(r, EVAL_REALM_RETRY_DELAY_MS));
|
|
1019
1190
|
continue;
|
|
1020
1191
|
}
|
|
1021
1192
|
throw err;
|
|
@@ -1049,14 +1220,10 @@ export class Browser {
|
|
|
1049
1220
|
* ```
|
|
1050
1221
|
*/
|
|
1051
1222
|
async addInitScript(fnOrSrc) {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
'BiDi is enabled by default — check that your browser supports it.');
|
|
1055
|
-
}
|
|
1223
|
+
this.requireBiDi('addInitScript()', 'addInitScript() requires BiDi. ' +
|
|
1224
|
+
'BiDi is enabled by default — check that your browser supports it.');
|
|
1056
1225
|
const conn = this.bidiSession.getConnection();
|
|
1057
|
-
const fnSrc = typeof fnOrSrc === 'function'
|
|
1058
|
-
? fnOrSrc.toString()
|
|
1059
|
-
: `() => { ${fnOrSrc} }`;
|
|
1226
|
+
const fnSrc = typeof fnOrSrc === 'function' ? fnOrSrc.toString() : `() => { ${fnOrSrc} }`;
|
|
1060
1227
|
const result = await conn.send('script.addPreloadScript', {
|
|
1061
1228
|
functionDeclaration: fnSrc,
|
|
1062
1229
|
});
|
|
@@ -1076,10 +1243,10 @@ export class Browser {
|
|
|
1076
1243
|
* Register **before** the action that triggers the request.
|
|
1077
1244
|
*/
|
|
1078
1245
|
waitForRequest(pattern, opts) {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1246
|
+
this.requireBiDi('waitForRequest()', 'waitForRequest() requires BiDi. BiDi is enabled by default — check your browser supports it.');
|
|
1247
|
+
return this.network.waitForRequest(pattern, {
|
|
1248
|
+
timeout: opts?.timeout ?? this.defaults.navigationTimeout,
|
|
1249
|
+
});
|
|
1083
1250
|
}
|
|
1084
1251
|
/**
|
|
1085
1252
|
* Wait for the first completed response matching a URL glob or predicate.
|
|
@@ -1095,16 +1262,14 @@ export class Browser {
|
|
|
1095
1262
|
* ```
|
|
1096
1263
|
*/
|
|
1097
1264
|
waitForResponse(pattern, opts) {
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1265
|
+
this.requireBiDi('waitForResponse()', 'waitForResponse() requires BiDi. BiDi is enabled by default \u2014 check your browser supports it.');
|
|
1266
|
+
return this.network.waitForResponse(pattern, {
|
|
1267
|
+
timeout: opts?.timeout ?? this.defaults.navigationTimeout,
|
|
1268
|
+
});
|
|
1102
1269
|
}
|
|
1103
1270
|
on(event, listener) {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
'Network event listeners use the W3C BiDi `network` module, which has no Classic-WebDriver equivalent.');
|
|
1107
|
-
}
|
|
1271
|
+
this.requireBiDi(`browser.on('${event}')`, `browser.on('${event}') requires BiDi (enableBiDi: true). ` +
|
|
1272
|
+
'Network event listeners use the W3C BiDi `network` module, which has no Classic-WebDriver equivalent.');
|
|
1108
1273
|
if (event === 'request') {
|
|
1109
1274
|
return this.network.on('request', listener);
|
|
1110
1275
|
}
|
|
@@ -1120,6 +1285,17 @@ export class Browser {
|
|
|
1120
1285
|
* await dl.saveAs('/tmp/report.csv');
|
|
1121
1286
|
*/
|
|
1122
1287
|
async waitForDownload(action, opts) {
|
|
1288
|
+
if (this._browserName === 'safari') {
|
|
1289
|
+
// safaridriver exposes no download-directory configuration craftdriver
|
|
1290
|
+
// can manage, so a browser download never lands in _downloadsDir — it
|
|
1291
|
+
// goes to the user's ~/Downloads. Without this guard, waitForDownload()
|
|
1292
|
+
// would watch an empty temp dir and fail with an opaque timeout instead
|
|
1293
|
+
// of a clear "unsupported" error. Craftdriver-managed downloads are out
|
|
1294
|
+
// of scope for Safari.
|
|
1295
|
+
throw new CraftdriverError(ErrorCode.UNSUPPORTED, 'waitForDownload() is not supported on Safari: safaridriver exposes no ' +
|
|
1296
|
+
'download-directory configuration craftdriver can manage, so downloads ' +
|
|
1297
|
+
'cannot be routed to or observed in a controlled directory.', { detail: { browserName: 'safari', feature: 'waitForDownload()' } });
|
|
1298
|
+
}
|
|
1123
1299
|
const dir = this._downloadsDir;
|
|
1124
1300
|
if (!dir) {
|
|
1125
1301
|
throw new Error('waitForDownload() requires a downloads directory. Browser was not launched correctly.');
|
|
@@ -1132,7 +1308,7 @@ export class Browser {
|
|
|
1132
1308
|
const deadline = Date.now() + timeout;
|
|
1133
1309
|
while (Date.now() < deadline) {
|
|
1134
1310
|
const after = fsSync.readdirSync(dir);
|
|
1135
|
-
const newFiles = after.filter(f => !before.has(f) && !f.endsWith('.crdownload'));
|
|
1311
|
+
const newFiles = after.filter((f) => !before.has(f) && !f.endsWith('.crdownload'));
|
|
1136
1312
|
if (newFiles.length > 0) {
|
|
1137
1313
|
const filename = newFiles[0];
|
|
1138
1314
|
const filePath = path.join(dir, filename);
|
|
@@ -1144,7 +1320,7 @@ export class Browser {
|
|
|
1144
1320
|
},
|
|
1145
1321
|
};
|
|
1146
1322
|
}
|
|
1147
|
-
await new Promise(r => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
1323
|
+
await new Promise((r) => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
1148
1324
|
}
|
|
1149
1325
|
throw new Error(`waitForDownload() timed out after ${timeout}ms — no file appeared in ${dir}`);
|
|
1150
1326
|
}
|
|
@@ -1171,9 +1347,27 @@ export class Browser {
|
|
|
1171
1347
|
await handler(dialog);
|
|
1172
1348
|
});
|
|
1173
1349
|
}
|
|
1174
|
-
|
|
1350
|
+
if (this._browserName === 'safari') {
|
|
1351
|
+
// Safari is Classic-only and has no WebDriver BiDi push events, so there
|
|
1352
|
+
// is no event-driven way to detect a dialog opening. A polling fallback
|
|
1353
|
+
// (repeatedly calling getAlertText()/driver "no such alert" errors to
|
|
1354
|
+
// detect a dialog) was considered, but would need to be *proven*
|
|
1355
|
+
// reliable on real Safari before shipping. Rather than silently no-op (today's
|
|
1356
|
+
// Classic behavior for other browsers) or let waitForDialog() hang
|
|
1357
|
+
// until its own generic timeout — which would look like a missed
|
|
1358
|
+
// dialog rather than "this API isn't supported here" — fail
|
|
1359
|
+
// immediately with a clear, actionable error. Revisit if a polling
|
|
1360
|
+
// form is later measured reliable on real Safari.
|
|
1361
|
+
throw new CraftdriverError(ErrorCode.UNSUPPORTED, 'onDialog() / waitForDialog() are not supported on Safari: Safari has no ' +
|
|
1362
|
+
'WebDriver BiDi, so there is no push-event mechanism for dialogs. ' +
|
|
1363
|
+
'Use the imperative dialog API instead — acceptDialog(), dismissDialog(), ' +
|
|
1364
|
+
'getDialogMessage() — which work in Classic mode.', { detail: { browserName: 'safari', feature: 'onDialog()' } });
|
|
1365
|
+
}
|
|
1366
|
+
// Classic (non-Safari): no push events — callers must use the imperative API below
|
|
1175
1367
|
// Return a no-op unsubscribe
|
|
1176
|
-
return () => {
|
|
1368
|
+
return () => {
|
|
1369
|
+
/* no-op */
|
|
1370
|
+
};
|
|
1177
1371
|
}
|
|
1178
1372
|
/**
|
|
1179
1373
|
* Accept the currently open dialog (OK / confirm).
|
|
@@ -1230,15 +1424,31 @@ export class Browser {
|
|
|
1230
1424
|
waitForDialog(opts) {
|
|
1231
1425
|
const timeout = opts?.timeout ?? this.defaults.timeout;
|
|
1232
1426
|
return new Promise((resolve, reject) => {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1427
|
+
let tid;
|
|
1428
|
+
let off;
|
|
1429
|
+
// onDialog() throws synchronously on Safari (no BiDi push events for
|
|
1430
|
+
// dialogs — see onDialog()'s comment). Register the handler before
|
|
1431
|
+
// scheduling the timeout so that throw rejects this promise
|
|
1432
|
+
// immediately instead of leaving a dangling timer whose callback would
|
|
1433
|
+
// later reference `off` before it was ever assigned.
|
|
1434
|
+
try {
|
|
1435
|
+
off = this.onDialog((dialog) => {
|
|
1436
|
+
if (tid !== undefined)
|
|
1437
|
+
clearTimeout(tid);
|
|
1438
|
+
off();
|
|
1439
|
+
resolve(dialog);
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
catch (err) {
|
|
1443
|
+
reject(err);
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
if (timeout > 0) {
|
|
1447
|
+
tid = setTimeout(() => {
|
|
1448
|
+
off();
|
|
1449
|
+
reject(new Error(`waitForDialog timed out after ${timeout}ms`));
|
|
1450
|
+
}, timeout);
|
|
1451
|
+
}
|
|
1242
1452
|
});
|
|
1243
1453
|
}
|
|
1244
1454
|
_buildDialog(params) {
|
|
@@ -1249,8 +1459,12 @@ export class Browser {
|
|
|
1249
1459
|
type: () => _type,
|
|
1250
1460
|
message: () => _message,
|
|
1251
1461
|
defaultValue: () => _defaultValue,
|
|
1252
|
-
accept: async (text) => {
|
|
1253
|
-
|
|
1462
|
+
accept: async (text) => {
|
|
1463
|
+
await this.acceptDialog(text);
|
|
1464
|
+
},
|
|
1465
|
+
dismiss: async () => {
|
|
1466
|
+
await this.dismissDialog();
|
|
1467
|
+
},
|
|
1254
1468
|
};
|
|
1255
1469
|
}
|
|
1256
1470
|
// ─── Frames ───────────────────────────────────────────────────────────────
|
|
@@ -1276,7 +1490,9 @@ export class Browser {
|
|
|
1276
1490
|
const topContextId = this.bidiSession.getContext();
|
|
1277
1491
|
if (topContextId) {
|
|
1278
1492
|
try {
|
|
1279
|
-
const tree = await conn.send('browsingContext.getTree', {
|
|
1493
|
+
const tree = await conn.send('browsingContext.getTree', {
|
|
1494
|
+
root: topContextId,
|
|
1495
|
+
});
|
|
1280
1496
|
// The BiDi context for the iframe has the top-level context as ancestor.
|
|
1281
1497
|
// We find the first child context (iframes appear as children).
|
|
1282
1498
|
const iframeSrc = await el.getAttribute('src');
|
|
@@ -1304,7 +1520,9 @@ export class Browser {
|
|
|
1304
1520
|
let tree;
|
|
1305
1521
|
if (conn && topContextId) {
|
|
1306
1522
|
try {
|
|
1307
|
-
tree = await conn.send('browsingContext.getTree', {
|
|
1523
|
+
tree = await conn.send('browsingContext.getTree', {
|
|
1524
|
+
root: topContextId,
|
|
1525
|
+
});
|
|
1308
1526
|
}
|
|
1309
1527
|
catch {
|
|
1310
1528
|
tree = undefined;
|
|
@@ -1328,7 +1546,9 @@ export class Browser {
|
|
|
1328
1546
|
async pages() {
|
|
1329
1547
|
if (this.bidiSession?.isConnected()) {
|
|
1330
1548
|
const conn = this.bidiSession.getConnection();
|
|
1331
|
-
const tree = await conn.send('browsingContext.getTree', {
|
|
1549
|
+
const tree = await conn.send('browsingContext.getTree', {
|
|
1550
|
+
maxDepth: 0,
|
|
1551
|
+
});
|
|
1332
1552
|
return (tree.contexts ?? []).map((ctx) => new Page(this.driver, ctx.context, this.getDefaultTimeout, conn, this._wrapContext(ctx.userContext ?? 'default')));
|
|
1333
1553
|
}
|
|
1334
1554
|
// Classic fallback: use window handles
|
|
@@ -1351,10 +1571,8 @@ export class Browser {
|
|
|
1351
1571
|
* await page.waitForLoadState();
|
|
1352
1572
|
*/
|
|
1353
1573
|
async openPage(opts) {
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
'WebDriver Classic cannot create top-level browsing contexts.');
|
|
1357
|
-
}
|
|
1574
|
+
this.requireBiDi('openPage()', 'openPage() requires BiDi (enableBiDi: true). ' +
|
|
1575
|
+
'WebDriver Classic cannot create top-level browsing contexts.');
|
|
1358
1576
|
const conn = this.bidiSession.getConnection();
|
|
1359
1577
|
const created = await conn.send('browsingContext.create', {
|
|
1360
1578
|
type: opts?.type ?? 'tab',
|
|
@@ -1365,16 +1583,11 @@ export class Browser {
|
|
|
1365
1583
|
}
|
|
1366
1584
|
return page;
|
|
1367
1585
|
}
|
|
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) {
|
|
1586
|
+
async waitForPage(actionOrMatcher, opts) {
|
|
1587
|
+
if (typeof actionOrMatcher !== 'function') {
|
|
1588
|
+
return this._waitForMatchingPage(actionOrMatcher, opts);
|
|
1589
|
+
}
|
|
1590
|
+
const action = actionOrMatcher;
|
|
1378
1591
|
const timeout = opts?.timeout ?? this.defaults.navigationTimeout;
|
|
1379
1592
|
if (this.bidiSession?.isConnected()) {
|
|
1380
1593
|
const conn = this.bidiSession.getConnection();
|
|
@@ -1407,15 +1620,60 @@ export class Browser {
|
|
|
1407
1620
|
const deadline = Date.now() + timeout;
|
|
1408
1621
|
while (Date.now() < deadline) {
|
|
1409
1622
|
const after = await this.driver.getWindowHandles();
|
|
1410
|
-
const newHandles = after.filter(h => !before.has(h));
|
|
1623
|
+
const newHandles = after.filter((h) => !before.has(h));
|
|
1411
1624
|
if (newHandles.length > 0) {
|
|
1412
1625
|
const handle = newHandles[0];
|
|
1413
1626
|
return new Page(this.driver, handle, this.getDefaultTimeout);
|
|
1414
1627
|
}
|
|
1415
|
-
await new Promise(r => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
1628
|
+
await new Promise((r) => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
1416
1629
|
}
|
|
1417
1630
|
throw new Error(`waitForPage() timed out after ${timeout}ms — no new tab or popup appeared`);
|
|
1418
1631
|
}
|
|
1632
|
+
/** Poll open top-level pages until one matches `matcher`; see {@link waitForPage}. */
|
|
1633
|
+
async _waitForMatchingPage(matcher, opts) {
|
|
1634
|
+
if (matcher.url === undefined && matcher.title === undefined) {
|
|
1635
|
+
throw new Error('waitForPage(matcher): provide at least one of { url, title }.');
|
|
1636
|
+
}
|
|
1637
|
+
const timeout = opts?.timeout ?? this.defaults.navigationTimeout;
|
|
1638
|
+
const deadline = Date.now() + timeout;
|
|
1639
|
+
const seen = new Set();
|
|
1640
|
+
for (;;) {
|
|
1641
|
+
for (const page of await this.pages()) {
|
|
1642
|
+
try {
|
|
1643
|
+
if (await this._pageMatches(page, matcher, seen)) {
|
|
1644
|
+
// Classic: make the matched window current so browser.find(...) targets
|
|
1645
|
+
// it too (BiDi drives through the returned Page's own context).
|
|
1646
|
+
if (!this.bidiSession?.isConnected()) {
|
|
1647
|
+
await this.driver.switchToWindow(page.id()).catch(() => { });
|
|
1648
|
+
}
|
|
1649
|
+
return page;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
catch (err) {
|
|
1653
|
+
// A window that closed mid-check (e.g. the splash) — skip it, keep looking.
|
|
1654
|
+
if (!isNoSuchWindowError(err))
|
|
1655
|
+
throw err;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
if (Date.now() >= deadline) {
|
|
1659
|
+
const label = [
|
|
1660
|
+
matcher.title !== undefined ? `title ${String(matcher.title)}` : undefined,
|
|
1661
|
+
matcher.url !== undefined ? `url ${String(matcher.url)}` : undefined,
|
|
1662
|
+
]
|
|
1663
|
+
.filter(Boolean)
|
|
1664
|
+
.join(' and ');
|
|
1665
|
+
throw new Error(`waitForPage(matcher) timed out after ${timeout}ms — no window matched ${label}. ` +
|
|
1666
|
+
`Saw: ${[...seen].join(' | ') || '(no readable windows)'}.`);
|
|
1667
|
+
}
|
|
1668
|
+
await new Promise((r) => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
async _pageMatches(page, matcher, seen) {
|
|
1672
|
+
const url = matcher.url !== undefined ? await page.url() : undefined;
|
|
1673
|
+
const title = matcher.title !== undefined ? await page.title() : undefined;
|
|
1674
|
+
seen.add([title !== undefined ? `"${title}"` : '', url ?? ''].filter(Boolean).join(' '));
|
|
1675
|
+
return matchPageFields({ url, title }, matcher);
|
|
1676
|
+
}
|
|
1419
1677
|
// ─── User contexts (isolated profiles, BiDi-only) ────────────────────────
|
|
1420
1678
|
/**
|
|
1421
1679
|
* Create a new isolated user context (an "incognito profile"). Each
|
|
@@ -1472,14 +1730,14 @@ export class Browser {
|
|
|
1472
1730
|
this._contextsById.set(id, ctx);
|
|
1473
1731
|
// Evict on close so long-running suites that create many contexts don't
|
|
1474
1732
|
// accumulate dead wrappers.
|
|
1475
|
-
ctx.on('close', () => {
|
|
1733
|
+
ctx.on('close', () => {
|
|
1734
|
+
this._contextsById.delete(id);
|
|
1735
|
+
});
|
|
1476
1736
|
return ctx;
|
|
1477
1737
|
}
|
|
1478
1738
|
async newContext(opts) {
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
'WebDriver Classic has no concept of user contexts.');
|
|
1482
|
-
}
|
|
1739
|
+
this.requireBiDi('newContext()', 'newContext() requires BiDi (enableBiDi: true). ' +
|
|
1740
|
+
'WebDriver Classic has no concept of user contexts.');
|
|
1483
1741
|
const conn = this.bidiSession.getConnection();
|
|
1484
1742
|
const created = await conn.send('browser.createUserContext', {});
|
|
1485
1743
|
const ctx = this._wrapContext(created.userContext, {
|
|
@@ -1489,7 +1747,7 @@ export class Browser {
|
|
|
1489
1747
|
if (opts?.storageState !== undefined) {
|
|
1490
1748
|
await ctx.loadStorageState(opts.storageState);
|
|
1491
1749
|
}
|
|
1492
|
-
// Apply
|
|
1750
|
+
// Apply identity & device emulation options. Each setter is `userContexts`-scoped,
|
|
1493
1751
|
// so future pages in this context inherit automatically.
|
|
1494
1752
|
if (opts?.locale !== undefined)
|
|
1495
1753
|
await ctx.setLocale(opts.locale);
|
|
@@ -1505,11 +1763,9 @@ export class Browser {
|
|
|
1505
1763
|
* Maps to BiDi `browser.getUserContexts`. **BiDi-only.**
|
|
1506
1764
|
*/
|
|
1507
1765
|
async contexts() {
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
'Use browser.pages() to list tabs instead.');
|
|
1512
|
-
}
|
|
1766
|
+
this.requireBiDi('contexts()', 'contexts() requires BiDi (enableBiDi: true). ' +
|
|
1767
|
+
'WebDriver Classic has no concept of user contexts. ' +
|
|
1768
|
+
'Use browser.pages() to list tabs instead.');
|
|
1513
1769
|
const conn = this.bidiSession.getConnection();
|
|
1514
1770
|
const result = await conn.send('browser.getUserContexts', {});
|
|
1515
1771
|
return (result.userContexts ?? []).map((uc) => this._wrapContext(uc.userContext));
|
|
@@ -1520,10 +1776,8 @@ export class Browser {
|
|
|
1520
1776
|
* to this context. **BiDi-only.**
|
|
1521
1777
|
*/
|
|
1522
1778
|
get defaultContext() {
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
'WebDriver Classic has no concept of user contexts.');
|
|
1526
|
-
}
|
|
1779
|
+
this.requireBiDi('defaultContext', 'defaultContext requires BiDi (enableBiDi: true). ' +
|
|
1780
|
+
'WebDriver Classic has no concept of user contexts.');
|
|
1527
1781
|
if (this._defaultContext)
|
|
1528
1782
|
return this._defaultContext;
|
|
1529
1783
|
this._defaultContext = this._wrapContext('default');
|
|
@@ -1561,7 +1815,9 @@ export class Browser {
|
|
|
1561
1815
|
// would break that contract if a future refactor made the branch reachable.
|
|
1562
1816
|
await conn
|
|
1563
1817
|
.subscribe(['browsingContext.contextCreated', 'browsingContext.contextDestroyed'])
|
|
1564
|
-
.catch(() => {
|
|
1818
|
+
.catch(() => {
|
|
1819
|
+
/* already subscribed or unsupported; fallback sync will cover us */
|
|
1820
|
+
});
|
|
1565
1821
|
}
|
|
1566
1822
|
const onCreated = (params) => {
|
|
1567
1823
|
if (params.parent)
|
|
@@ -1596,7 +1852,9 @@ export class Browser {
|
|
|
1596
1852
|
}
|
|
1597
1853
|
async _refreshTopLevelContextCache(conn) {
|
|
1598
1854
|
const version = this._topLevelContextCacheVersion;
|
|
1599
|
-
const tree = await conn.send('browsingContext.getTree', {
|
|
1855
|
+
const tree = await conn.send('browsingContext.getTree', {
|
|
1856
|
+
maxDepth: 0,
|
|
1857
|
+
});
|
|
1600
1858
|
if (this._topLevelContextCacheVersion === version) {
|
|
1601
1859
|
this._topLevelContextUserContexts.clear();
|
|
1602
1860
|
}
|
|
@@ -1653,8 +1911,30 @@ export class Browser {
|
|
|
1653
1911
|
}
|
|
1654
1912
|
return new Page(this.driver, fallback, this.getDefaultTimeout, conn, this.defaultContext);
|
|
1655
1913
|
}
|
|
1656
|
-
// Classic mode:
|
|
1657
|
-
|
|
1914
|
+
// Classic mode: wrap the current window. If it was closed (e.g. a splash that
|
|
1915
|
+
// replaced itself), re-point to the sole remaining top-level window when there's
|
|
1916
|
+
// exactly one — unambiguous — and otherwise ask the caller to select explicitly,
|
|
1917
|
+
// rather than guessing among several or throwing an opaque "no such window".
|
|
1918
|
+
let handle;
|
|
1919
|
+
try {
|
|
1920
|
+
handle = await this.driver.getCurrentWindowHandle();
|
|
1921
|
+
}
|
|
1922
|
+
catch (err) {
|
|
1923
|
+
if (!isNoSuchWindowError(err))
|
|
1924
|
+
throw err;
|
|
1925
|
+
}
|
|
1926
|
+
const handles = await this.driver.getWindowHandles();
|
|
1927
|
+
if (handle === undefined || !handles.includes(handle)) {
|
|
1928
|
+
if (handles.length === 0) {
|
|
1929
|
+
throw new Error('activePage(): all windows are closed.');
|
|
1930
|
+
}
|
|
1931
|
+
if (handles.length > 1) {
|
|
1932
|
+
throw new Error('activePage(): the current window was closed and several windows are open. ' +
|
|
1933
|
+
'Select one with browser.waitForPage({ title } | { url }) or browser.pages().');
|
|
1934
|
+
}
|
|
1935
|
+
handle = handles[0];
|
|
1936
|
+
await this.driver.switchToWindow(handle).catch(() => { });
|
|
1937
|
+
}
|
|
1658
1938
|
return new Page(this.driver, handle, this.getDefaultTimeout);
|
|
1659
1939
|
}
|
|
1660
1940
|
async click(selector, opts) {
|
|
@@ -1695,12 +1975,31 @@ export class Browser {
|
|
|
1695
1975
|
}
|
|
1696
1976
|
gesture = {
|
|
1697
1977
|
swipe: async ({ from, to, durationMs = 300, }) => {
|
|
1978
|
+
this.rejectTouchActionsOnSafari('gesture.swipe()');
|
|
1698
1979
|
await this.driver.performTouchSwipe(from, to, durationMs);
|
|
1699
1980
|
},
|
|
1700
1981
|
pinch: async ({ center, scale = 0.5, distance = 100, durationMs = 250, }) => {
|
|
1982
|
+
this.rejectTouchActionsOnSafari('gesture.pinch()');
|
|
1701
1983
|
await this.driver.performTouchPinch(center, scale, distance, durationMs);
|
|
1702
1984
|
},
|
|
1703
1985
|
};
|
|
1986
|
+
/**
|
|
1987
|
+
* `gesture.swipe()`/`gesture.pinch()` send W3C actions with a `pointer`
|
|
1988
|
+
* input source whose `parameters.pointerType` is `'touch'`. Desktop Safari
|
|
1989
|
+
* has no touchscreen and Apple's Safari 12+ WebDriver command table gives
|
|
1990
|
+
* no indication `safaridriver` synthesizes touch-type pointer input —
|
|
1991
|
+
* unlike `pointerType: 'mouse'`, which is exercised by every other pointer
|
|
1992
|
+
* helper and is squarely in scope. Rather than let the action silently
|
|
1993
|
+
* no-op (or behave unpredictably) on real Safari, fail loudly before
|
|
1994
|
+
* sending it.
|
|
1995
|
+
*/
|
|
1996
|
+
rejectTouchActionsOnSafari(feature) {
|
|
1997
|
+
if (this._browserName !== 'safari')
|
|
1998
|
+
return;
|
|
1999
|
+
throw new CraftdriverError(ErrorCode.UNSUPPORTED, `${feature} sends a touch-type pointer action, which desktop Safari does not support. ` +
|
|
2000
|
+
'Safari has no touchscreen and no documented touch-pointer WebDriver behavior; ' +
|
|
2001
|
+
'this is desktop browser automation, not iPhone/iPad Safari coverage.', { detail: { browserName: 'safari', feature } });
|
|
2002
|
+
}
|
|
1704
2003
|
/**
|
|
1705
2004
|
* Capture a screenshot of the active page (viewport by default), the
|
|
1706
2005
|
* full scrollable document (`fullPage: true`), or an element matching
|
|
@@ -1733,11 +2032,9 @@ export class Browser {
|
|
|
1733
2032
|
buf = Buffer.from(b64, 'base64');
|
|
1734
2033
|
}
|
|
1735
2034
|
else if (opts?.fullPage) {
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
'command with `origin: "document"`, which has no Classic-WebDriver equivalent.');
|
|
1740
|
-
}
|
|
2035
|
+
this.requireBiDi('screenshot({ fullPage: true })', 'screenshot({ fullPage: true }) requires BiDi (enableBiDi: true). ' +
|
|
2036
|
+
'Full-page screenshots use the W3C BiDi `browsingContext.captureScreenshot` ' +
|
|
2037
|
+
'command with `origin: "document"`, which has no Classic-WebDriver equivalent.');
|
|
1741
2038
|
const conn = this.bidiSession.getConnection();
|
|
1742
2039
|
const page = await this.activePage();
|
|
1743
2040
|
const result = await conn.send('browsingContext.captureScreenshot', {
|
|
@@ -1807,19 +2104,27 @@ export class Browser {
|
|
|
1807
2104
|
static Key = Key;
|
|
1808
2105
|
async waitForVisible(selector, opts) {
|
|
1809
2106
|
const by = typeof selector === 'string' ? By.css(selector) : selector;
|
|
1810
|
-
await this.driver.wait(until.elementIsVisible(by), {
|
|
2107
|
+
await this.driver.wait(until.elementIsVisible(by), {
|
|
2108
|
+
timeout: opts?.timeout ?? this.defaults.timeout,
|
|
2109
|
+
});
|
|
1811
2110
|
}
|
|
1812
2111
|
async waitForHidden(selector, opts) {
|
|
1813
2112
|
const by = typeof selector === 'string' ? By.css(selector) : selector;
|
|
1814
|
-
await this.driver.wait(until.elementIsNotVisible(by), {
|
|
2113
|
+
await this.driver.wait(until.elementIsNotVisible(by), {
|
|
2114
|
+
timeout: opts?.timeout ?? this.defaults.timeout,
|
|
2115
|
+
});
|
|
1815
2116
|
}
|
|
1816
2117
|
async waitForAttached(selector, opts) {
|
|
1817
2118
|
const by = typeof selector === 'string' ? By.css(selector) : selector;
|
|
1818
|
-
await this.driver.wait(until.elementExists(by), {
|
|
2119
|
+
await this.driver.wait(until.elementExists(by), {
|
|
2120
|
+
timeout: opts?.timeout ?? this.defaults.timeout,
|
|
2121
|
+
});
|
|
1819
2122
|
}
|
|
1820
2123
|
async waitForDetached(selector, opts) {
|
|
1821
2124
|
const by = typeof selector === 'string' ? By.css(selector) : selector;
|
|
1822
|
-
await this.driver.wait(until.elementNotExists(by), {
|
|
2125
|
+
await this.driver.wait(until.elementNotExists(by), {
|
|
2126
|
+
timeout: opts?.timeout ?? this.defaults.timeout,
|
|
2127
|
+
});
|
|
1823
2128
|
}
|
|
1824
2129
|
/**
|
|
1825
2130
|
* Wait for an element to reach the given state.
|
|
@@ -1833,10 +2138,14 @@ export class Browser {
|
|
|
1833
2138
|
*/
|
|
1834
2139
|
async waitFor(selector, opts) {
|
|
1835
2140
|
switch (opts.state) {
|
|
1836
|
-
case 'visible':
|
|
1837
|
-
|
|
1838
|
-
case '
|
|
1839
|
-
|
|
2141
|
+
case 'visible':
|
|
2142
|
+
return this.waitForVisible(selector, opts);
|
|
2143
|
+
case 'hidden':
|
|
2144
|
+
return this.waitForHidden(selector, opts);
|
|
2145
|
+
case 'attached':
|
|
2146
|
+
return this.waitForAttached(selector, opts);
|
|
2147
|
+
case 'detached':
|
|
2148
|
+
return this.waitForDetached(selector, opts);
|
|
1840
2149
|
}
|
|
1841
2150
|
}
|
|
1842
2151
|
actions() {
|