craftdriver 0.2.2 → 1.0.1
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 +84 -0
- package/dist/cli/mcp/server.js +16 -3
- package/dist/cli/mcp/server.js.map +1 -1
- package/dist/lib/bidi/connection.d.ts +1 -1
- package/dist/lib/bidi/connection.d.ts.map +1 -1
- package/dist/lib/bidi/connection.js +3 -2
- package/dist/lib/bidi/connection.js.map +1 -1
- package/dist/lib/bidi/index.d.ts +40 -3
- package/dist/lib/bidi/index.d.ts.map +1 -1
- package/dist/lib/bidi/index.js +55 -18
- package/dist/lib/bidi/index.js.map +1 -1
- package/dist/lib/bidi/logs.d.ts.map +1 -1
- package/dist/lib/bidi/logs.js +7 -2
- package/dist/lib/bidi/logs.js.map +1 -1
- package/dist/lib/bidi/network.d.ts +9 -0
- package/dist/lib/bidi/network.d.ts.map +1 -1
- package/dist/lib/bidi/network.js +26 -11
- package/dist/lib/bidi/network.js.map +1 -1
- package/dist/lib/browser.d.ts +42 -4
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +140 -62
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/browserContext.d.ts +7 -2
- package/dist/lib/browserContext.d.ts.map +1 -1
- package/dist/lib/browserContext.js +15 -6
- package/dist/lib/browserContext.js.map +1 -1
- package/dist/lib/builder.d.ts +2 -1
- package/dist/lib/builder.d.ts.map +1 -1
- package/dist/lib/builder.js +35 -5
- package/dist/lib/builder.js.map +1 -1
- package/dist/lib/clickFastPath.d.ts +7 -0
- package/dist/lib/clickFastPath.d.ts.map +1 -0
- package/dist/lib/clickFastPath.js +39 -0
- package/dist/lib/clickFastPath.js.map +1 -0
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +21 -6
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/driverManager.d.ts +6 -0
- package/dist/lib/driverManager.d.ts.map +1 -1
- package/dist/lib/driverManager.js +97 -22
- package/dist/lib/driverManager.js.map +1 -1
- package/dist/lib/elementHandle.d.ts.map +1 -1
- package/dist/lib/elementHandle.js +7 -2
- package/dist/lib/elementHandle.js.map +1 -1
- package/dist/lib/expect.d.ts.map +1 -1
- package/dist/lib/expect.js +91 -229
- package/dist/lib/expect.js.map +1 -1
- package/dist/lib/firefox.d.ts.map +1 -1
- package/dist/lib/firefox.js +2 -1
- package/dist/lib/firefox.js.map +1 -1
- package/dist/lib/frame.d.ts.map +1 -1
- package/dist/lib/frame.js +7 -7
- package/dist/lib/frame.js.map +1 -1
- package/dist/lib/http.d.ts +7 -0
- package/dist/lib/http.d.ts.map +1 -1
- package/dist/lib/http.js +112 -3
- package/dist/lib/http.js.map +1 -1
- package/dist/lib/loadState.d.ts +23 -0
- package/dist/lib/loadState.d.ts.map +1 -0
- package/dist/lib/loadState.js +25 -0
- package/dist/lib/loadState.js.map +1 -0
- package/dist/lib/locator.d.ts +2 -0
- package/dist/lib/locator.d.ts.map +1 -1
- package/dist/lib/locator.js +19 -6
- package/dist/lib/locator.js.map +1 -1
- package/dist/lib/mouse.d.ts.map +1 -1
- package/dist/lib/mouse.js +2 -1
- package/dist/lib/mouse.js.map +1 -1
- package/dist/lib/page.d.ts +14 -1
- package/dist/lib/page.d.ts.map +1 -1
- package/dist/lib/page.js +66 -15
- package/dist/lib/page.js.map +1 -1
- package/dist/lib/service.d.ts +1 -0
- package/dist/lib/service.d.ts.map +1 -1
- package/dist/lib/service.js +21 -3
- package/dist/lib/service.js.map +1 -1
- package/dist/lib/timing.d.ts +104 -0
- package/dist/lib/timing.d.ts.map +1 -0
- package/dist/lib/timing.js +108 -0
- package/dist/lib/timing.js.map +1 -0
- package/dist/lib/wait.d.ts.map +1 -1
- package/dist/lib/wait.js +13 -6
- package/dist/lib/wait.js.map +1 -1
- package/docs/bidi-features.md +10 -2
- package/docs/browser-api.md +2 -1
- package/docs/browser-context.md +11 -14
- package/docs/dialogs.md +14 -3
- package/docs/driver-configuration.md +126 -8
- package/docs/emulation.md +8 -4
- package/docs/error-codes.md +9 -4
- package/docs/keyboard-mouse.md +20 -7
- package/docs/session-management.md +1 -1
- package/package.json +4 -1
- package/skills/craftdriver/SKILL.md +3 -2
package/dist/lib/browser.js
CHANGED
|
@@ -3,6 +3,7 @@ import { ChromeService } from './chrome.js';
|
|
|
3
3
|
import { FirefoxService } from './firefox.js';
|
|
4
4
|
import { By } from './by.js';
|
|
5
5
|
import { until } from './wait.js';
|
|
6
|
+
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';
|
|
6
7
|
import { ElementHandle } from './elementHandle.js';
|
|
7
8
|
import { Locator } from './locator.js';
|
|
8
9
|
import { expectSelector } from './expect.js';
|
|
@@ -17,11 +18,13 @@ import { ActionsBuilder } from './actions.js';
|
|
|
17
18
|
import { BiDiSession, SessionStateManager, } from './bidi/index.js';
|
|
18
19
|
import { Frame } from './frame.js';
|
|
19
20
|
import { Page } from './page.js';
|
|
21
|
+
import { bidiWaitFor } from './loadState.js';
|
|
20
22
|
import { BrowserContext } from './browserContext.js';
|
|
21
23
|
import { Tracer } from './tracing.js';
|
|
22
24
|
import { A11y } from './a11y.js';
|
|
23
25
|
import { Clock } from './clock.js';
|
|
24
26
|
import { CraftdriverError, ErrorCode } from './errors.js';
|
|
27
|
+
import { clickWithFastPath } from './clickFastPath.js';
|
|
25
28
|
/** Common mobile device presets */
|
|
26
29
|
export const devices = {
|
|
27
30
|
'iPhone 14': {
|
|
@@ -180,13 +183,15 @@ export class Browser {
|
|
|
180
183
|
_storage;
|
|
181
184
|
_a11y;
|
|
182
185
|
_clock;
|
|
186
|
+
/** Browser-scoped BiDi preload scripts registered via addInitScript()/clock. */
|
|
187
|
+
_browserInitScriptIds = new Set();
|
|
183
188
|
_downloadsDir;
|
|
184
189
|
_driverService;
|
|
185
190
|
_browserName = 'chrome';
|
|
186
191
|
/** Active emulation overrides, re-applied to new top-level contexts. */
|
|
187
192
|
_emulation = {};
|
|
188
193
|
/** Mutable browser-level defaults. Use setDefaultTimeout() / setDefaultNavigationTimeout() to change. */
|
|
189
|
-
defaults = { timeout:
|
|
194
|
+
defaults = { timeout: DEFAULT_ELEMENT_TIMEOUT_MS, navigationTimeout: DEFAULT_NAVIGATION_TIMEOUT_MS };
|
|
190
195
|
constructor(driver) {
|
|
191
196
|
this.driver = driver;
|
|
192
197
|
this.keyboard = new Keyboard(this.driver);
|
|
@@ -208,6 +213,8 @@ export class Browser {
|
|
|
208
213
|
}
|
|
209
214
|
/** Returns the current default timeout. Used as a live getter passed to ElementHandle / expectSelector. */
|
|
210
215
|
getDefaultTimeout = () => this.defaults.timeout;
|
|
216
|
+
hasBrowserInitScripts = () => this._browserInitScriptIds.size > 0;
|
|
217
|
+
hasDefaultNavigationInitScripts = () => this._defaultContext?._hasInitScriptsForNavigation() ?? this.hasBrowserInitScripts();
|
|
211
218
|
static async launch(options = {}) {
|
|
212
219
|
const name = options.browserName ?? 'chrome';
|
|
213
220
|
const isFirefox = name === 'firefox';
|
|
@@ -225,7 +232,7 @@ export class Browser {
|
|
|
225
232
|
let caps = {};
|
|
226
233
|
if (isChromeFamily) {
|
|
227
234
|
const chromeOptions = {
|
|
228
|
-
args: isHeadless ? ['--headless=new'] : [],
|
|
235
|
+
args: [...(isHeadless ? ['--headless=new'] : []), ...(options.args ?? [])],
|
|
229
236
|
prefs: {
|
|
230
237
|
'download.default_directory': downloadsDir,
|
|
231
238
|
'download.prompt_for_download': false,
|
|
@@ -265,6 +272,8 @@ export class Browser {
|
|
|
265
272
|
const firefoxArgs = [];
|
|
266
273
|
if (isHeadless)
|
|
267
274
|
firefoxArgs.push('-headless');
|
|
275
|
+
if (options.args?.length)
|
|
276
|
+
firefoxArgs.push(...options.args);
|
|
268
277
|
caps['moz:firefoxOptions'] = {
|
|
269
278
|
args: firefoxArgs,
|
|
270
279
|
prefs: {
|
|
@@ -307,6 +316,10 @@ export class Browser {
|
|
|
307
316
|
const wsUrl = driver.__wsUrl;
|
|
308
317
|
if (wsUrl && options.enableBiDi !== false) {
|
|
309
318
|
await browser.initBiDi(wsUrl);
|
|
319
|
+
if (options.captureLogs && browser.bidiSession?.isConnected()) {
|
|
320
|
+
// Arm log capture now instead of lazily on first browser.logs touch.
|
|
321
|
+
void browser.bidiSession.logs.initialize();
|
|
322
|
+
}
|
|
310
323
|
}
|
|
311
324
|
// Load session state if provided
|
|
312
325
|
if (options.storageState) {
|
|
@@ -323,17 +336,24 @@ export class Browser {
|
|
|
323
336
|
// Retry up to 8 times with increasing delays — Firefox may not have finished
|
|
324
337
|
// binding its BiDi WebSocket yet (especially when a previous session just closed
|
|
325
338
|
// on the same port, or the profile is still initialising).
|
|
326
|
-
const maxAttempts =
|
|
339
|
+
const maxAttempts = BIDI_CONNECT_MAX_ATTEMPTS;
|
|
327
340
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
328
341
|
try {
|
|
329
|
-
await session.connect(wsUrl
|
|
342
|
+
await session.connect(wsUrl, {
|
|
343
|
+
// Seed the top-level-context cache from the tree connect() already
|
|
344
|
+
// fetched, off the subscription connect() already armed — avoids a
|
|
345
|
+
// second getTree/subscribe round trip right after this returns.
|
|
346
|
+
onContextTree: (contexts) => {
|
|
347
|
+
this._topLevelContextTracking = this._startTopLevelContextTracking(contexts);
|
|
348
|
+
},
|
|
349
|
+
});
|
|
330
350
|
await this._ensureTopLevelContextTracking().catch(() => { });
|
|
331
351
|
return; // success
|
|
332
352
|
}
|
|
333
353
|
catch (err) {
|
|
334
354
|
if (attempt < maxAttempts) {
|
|
335
355
|
// Back-off: 300ms, 600ms, 900ms, 1200ms …
|
|
336
|
-
await new Promise((r) => setTimeout(r,
|
|
356
|
+
await new Promise((r) => setTimeout(r, BIDI_CONNECT_BACKOFF_STEP_MS * attempt));
|
|
337
357
|
}
|
|
338
358
|
else {
|
|
339
359
|
// All attempts failed; fall back to Classic-only mode.
|
|
@@ -432,32 +452,30 @@ export class Browser {
|
|
|
432
452
|
async navigateTo(url, opts) {
|
|
433
453
|
this._tracer?.recordAction('navigateTo', [url, opts], url);
|
|
434
454
|
const waitUntil = opts?.waitUntil ?? 'load';
|
|
435
|
-
|
|
455
|
+
// Classic-first for ordinary `waitUntil: 'load'` navigations: Classic
|
|
456
|
+
// already blocks until `document.readyState === 'complete'`, so the common
|
|
457
|
+
// case avoids a BiDi round trip. Preload-backed sessions stay on BiDi
|
|
458
|
+
// because the next operation is often a BiDi script evaluation into the new
|
|
459
|
+
// document; mixing Classic navigate + immediate BiDi evaluate can race with
|
|
460
|
+
// the browser clearing old execution contexts on loaded-but-settling pages.
|
|
461
|
+
const needsBiDi = waitUntil !== 'load' || this.hasDefaultNavigationInitScripts();
|
|
462
|
+
const context = needsBiDi ? this.bidiSession?.getContext() : undefined;
|
|
463
|
+
if (context && this.bidiSession?.isConnected()) {
|
|
436
464
|
const conn = this.bidiSession.getConnection();
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// 'networkidle' uses 'complete' (load) then waits for network silence
|
|
444
|
-
const bidiWait = waitUntil === 'none' ? 'none'
|
|
445
|
-
: waitUntil === 'domcontentloaded' ? 'interactive'
|
|
446
|
-
: 'complete'; // 'load' and 'networkidle'
|
|
447
|
-
await conn.send('browsingContext.navigate', { context, url, wait: bidiWait });
|
|
448
|
-
if (waitUntil === 'networkidle') {
|
|
449
|
-
await this.bidiSession.network.waitForNetworkIdle({
|
|
450
|
-
timeout: this.defaults.navigationTimeout,
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
return;
|
|
465
|
+
const bidiWait = bidiWaitFor(waitUntil);
|
|
466
|
+
await conn.send('browsingContext.navigate', { context, url, wait: bidiWait });
|
|
467
|
+
if (waitUntil === 'networkidle') {
|
|
468
|
+
await this.bidiSession.network.waitForNetworkIdle({
|
|
469
|
+
timeout: this.defaults.navigationTimeout,
|
|
470
|
+
});
|
|
454
471
|
}
|
|
472
|
+
return;
|
|
455
473
|
}
|
|
456
|
-
// Classic
|
|
474
|
+
// Classic path — default 'load', or BiDi unavailable/no context yet.
|
|
457
475
|
await this.driver.navigateTo(url);
|
|
458
476
|
if (waitUntil === 'networkidle') {
|
|
459
477
|
// Best-effort: Classic can't track network events; give it a short settle time
|
|
460
|
-
await new Promise(r => setTimeout(r,
|
|
478
|
+
await new Promise(r => setTimeout(r, NETWORK_IDLE_SETTLE_MS));
|
|
461
479
|
}
|
|
462
480
|
}
|
|
463
481
|
/**
|
|
@@ -790,7 +808,7 @@ export class Browser {
|
|
|
790
808
|
}
|
|
791
809
|
// Classic fallback: best-effort 500ms settle
|
|
792
810
|
else {
|
|
793
|
-
await new Promise(r => setTimeout(r,
|
|
811
|
+
await new Promise(r => setTimeout(r, NETWORK_IDLE_SETTLE_MS));
|
|
794
812
|
}
|
|
795
813
|
return;
|
|
796
814
|
}
|
|
@@ -835,7 +853,7 @@ export class Browser {
|
|
|
835
853
|
const readyState = await this.driver.executeScript('return document.readyState', []);
|
|
836
854
|
if (readyState === 'complete' || (target === 'interactive' && readyState === 'interactive'))
|
|
837
855
|
return;
|
|
838
|
-
await new Promise(r => setTimeout(r,
|
|
856
|
+
await new Promise(r => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
839
857
|
}
|
|
840
858
|
throw new CraftdriverError(ErrorCode.TIMEOUT_WAITING_LOAD, `waitForLoadState('${state}') timed out after ${timeout}ms`, { detail: { state, timeout } });
|
|
841
859
|
}
|
|
@@ -852,10 +870,14 @@ export class Browser {
|
|
|
852
870
|
// Silently abort any running trace so we don't leak the timer.
|
|
853
871
|
if (this._tracer?.isRunning)
|
|
854
872
|
this._tracer.abort();
|
|
873
|
+
// Whether this session had a live BiDi WebSocket. Only the Firefox+BiDi
|
|
874
|
+
// combination needs the port-release pause below; capture it before close().
|
|
875
|
+
const bidiWasActive = !!this.bidiSession;
|
|
855
876
|
// Close BiDi connection first
|
|
856
877
|
if (this.bidiSession) {
|
|
857
878
|
await this.bidiSession.close().catch(() => { });
|
|
858
879
|
}
|
|
880
|
+
this._browserInitScriptIds.clear();
|
|
859
881
|
for (const off of this._topLevelContextTrackingOffs)
|
|
860
882
|
off();
|
|
861
883
|
this._topLevelContextTrackingOffs = [];
|
|
@@ -864,10 +886,16 @@ export class Browser {
|
|
|
864
886
|
this._topLevelContextCacheVersion++;
|
|
865
887
|
// DELETE the WebDriver session — this tells the driver service to close the browser.
|
|
866
888
|
await this.driver.quit().catch(() => { });
|
|
867
|
-
//
|
|
868
|
-
//
|
|
869
|
-
//
|
|
870
|
-
|
|
889
|
+
// Firefox's BiDi WebSocket binds a fixed port (9222). If we kill geckodriver
|
|
890
|
+
// before Firefox has released it, the next Firefox+BiDi launch can 404 while
|
|
891
|
+
// connecting to the new session's socket — hence this pause. Chrome has no such
|
|
892
|
+
// race: chromedriver assigns an ephemeral --remote-debugging-port=0 and our BiDi
|
|
893
|
+
// URL comes from the session's own webSocketUrl, not a fixed port. So scope the
|
|
894
|
+
// 500ms sleep to Firefox+BiDi; every other quit (all Chrome, Firefox w/o BiDi)
|
|
895
|
+
// skips straight to stopping the driver.
|
|
896
|
+
if (this._browserName === 'firefox' && bidiWasActive) {
|
|
897
|
+
await new Promise((r) => setTimeout(r, PORT_RELEASE_DELAY_MS));
|
|
898
|
+
}
|
|
871
899
|
// Stop the underlying driver service (chromedriver / geckodriver)
|
|
872
900
|
// so we don't leak processes between sessions.
|
|
873
901
|
if (this._driverService) {
|
|
@@ -932,26 +960,36 @@ export class Browser {
|
|
|
932
960
|
const fnSrc = typeof fn === 'function' ? fn.toString() : fn;
|
|
933
961
|
if (this.bidiSession?.isConnected()) {
|
|
934
962
|
const conn = this.bidiSession.getConnection();
|
|
935
|
-
|
|
936
|
-
|
|
963
|
+
// Classic-first navigations return at readyState === 'complete', which is
|
|
964
|
+
// not a barrier the BiDi side respects: an immediately following
|
|
965
|
+
// { context } call can race the browser swapping the old realm for the
|
|
966
|
+
// new one and throw "execution contexts cleared". The error is
|
|
967
|
+
// pre-execution (script never ran, no side effects), so retry a few times,
|
|
968
|
+
// re-resolving the context each attempt. In-script errors take the
|
|
969
|
+
// result.type === 'exception' path below and are never retried here.
|
|
970
|
+
const functionDeclaration = typeof fn === 'function' ? fnSrc : `function() { ${fnSrc} }`;
|
|
971
|
+
const callArgs = typeof fn === 'function' ? args.map(serializeLocalValue) : [];
|
|
937
972
|
let result;
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
973
|
+
for (let attempt = 1;; attempt++) {
|
|
974
|
+
const context = this.bidiSession.getContext();
|
|
975
|
+
const target = context ? { context } : {};
|
|
976
|
+
try {
|
|
977
|
+
result = await conn.send('script.callFunction', {
|
|
978
|
+
functionDeclaration,
|
|
979
|
+
target,
|
|
980
|
+
arguments: callArgs,
|
|
981
|
+
awaitPromise: true,
|
|
982
|
+
});
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
catch (err) {
|
|
986
|
+
if (attempt < EVAL_REALM_RETRY_ATTEMPTS &&
|
|
987
|
+
String(err?.message).includes('execution contexts cleared')) {
|
|
988
|
+
await new Promise(r => setTimeout(r, EVAL_REALM_RETRY_DELAY_MS));
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
throw err;
|
|
992
|
+
}
|
|
955
993
|
}
|
|
956
994
|
if (result.type === 'exception') {
|
|
957
995
|
throw new CraftdriverError(ErrorCode.EVAL_THREW, `evaluate() threw an exception in the page: ${result.exceptionDetails?.text ?? 'unknown error'}`, { detail: { exception: result.exceptionDetails?.text ?? null } });
|
|
@@ -993,9 +1031,13 @@ export class Browser {
|
|
|
993
1031
|
functionDeclaration: fnSrc,
|
|
994
1032
|
});
|
|
995
1033
|
const scriptId = result.script;
|
|
1034
|
+
this._browserInitScriptIds.add(scriptId);
|
|
996
1035
|
return {
|
|
997
1036
|
remove: async () => {
|
|
1037
|
+
if (!this._browserInitScriptIds.has(scriptId))
|
|
1038
|
+
return;
|
|
998
1039
|
await conn.send('script.removePreloadScript', { script: scriptId });
|
|
1040
|
+
this._browserInitScriptIds.delete(scriptId);
|
|
999
1041
|
},
|
|
1000
1042
|
};
|
|
1001
1043
|
}
|
|
@@ -1072,7 +1114,7 @@ export class Browser {
|
|
|
1072
1114
|
},
|
|
1073
1115
|
};
|
|
1074
1116
|
}
|
|
1075
|
-
await new Promise(r => setTimeout(r,
|
|
1117
|
+
await new Promise(r => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
1076
1118
|
}
|
|
1077
1119
|
throw new Error(`waitForDownload() timed out after ${timeout}ms — no file appeared in ${dir}`);
|
|
1078
1120
|
}
|
|
@@ -1304,8 +1346,9 @@ export class Browser {
|
|
|
1304
1346
|
const timeout = opts?.timeout ?? this.defaults.navigationTimeout;
|
|
1305
1347
|
if (this.bidiSession?.isConnected()) {
|
|
1306
1348
|
const conn = this.bidiSession.getConnection();
|
|
1307
|
-
//
|
|
1308
|
-
|
|
1349
|
+
// `browsingContext.contextCreated` is already subscribed session-wide by
|
|
1350
|
+
// `BiDiSession.connect()` (which must have run for `isConnected()` to be
|
|
1351
|
+
// true), so no per-call `subscribe()` round trip is needed here.
|
|
1309
1352
|
return new Promise((resolve, reject) => {
|
|
1310
1353
|
const timer = setTimeout(() => {
|
|
1311
1354
|
off();
|
|
@@ -1337,7 +1380,7 @@ export class Browser {
|
|
|
1337
1380
|
const handle = newHandles[0];
|
|
1338
1381
|
return new Page(this.driver, handle, this.getDefaultTimeout);
|
|
1339
1382
|
}
|
|
1340
|
-
await new Promise(r => setTimeout(r,
|
|
1383
|
+
await new Promise(r => setTimeout(r, STATE_POLL_INTERVAL_MS));
|
|
1341
1384
|
}
|
|
1342
1385
|
throw new Error(`waitForPage() timed out after ${timeout}ms — no new tab or popup appeared`);
|
|
1343
1386
|
}
|
|
@@ -1389,7 +1432,11 @@ export class Browser {
|
|
|
1389
1432
|
if (cached)
|
|
1390
1433
|
return cached;
|
|
1391
1434
|
const conn = this.bidiSession.getConnection();
|
|
1392
|
-
const ctx = new BrowserContext(this.driver, conn, id, this.getDefaultTimeout, () => this.defaults.navigationTimeout, {
|
|
1435
|
+
const ctx = new BrowserContext(this.driver, conn, id, this.getDefaultTimeout, () => this.defaults.navigationTimeout, {
|
|
1436
|
+
getNetwork: () => this.network,
|
|
1437
|
+
getBrowserName: () => this._browserName,
|
|
1438
|
+
hasBrowserInitScripts: this.hasBrowserInitScripts,
|
|
1439
|
+
}, config);
|
|
1393
1440
|
this._contextsById.set(id, ctx);
|
|
1394
1441
|
// Evict on close so long-running suites that create many contexts don't
|
|
1395
1442
|
// accumulate dead wrappers.
|
|
@@ -1458,11 +1505,32 @@ export class Browser {
|
|
|
1458
1505
|
this._topLevelContextTracking = this._startTopLevelContextTracking();
|
|
1459
1506
|
return this._topLevelContextTracking;
|
|
1460
1507
|
}
|
|
1461
|
-
|
|
1508
|
+
/**
|
|
1509
|
+
* @param initialContexts When provided (the normal path — passed via
|
|
1510
|
+
* `BiDiSession.connect()`'s `onContextTree` callback), `contextCreated`/
|
|
1511
|
+
* `contextDestroyed` are already subscribed as part of `connect()`'s
|
|
1512
|
+
* merged batch and the tree is already fetched — this just registers
|
|
1513
|
+
* handlers and seeds the cache from it, no extra round trip. Falls back
|
|
1514
|
+
* to its own subscribe + `getTree` when called without a pre-fetched
|
|
1515
|
+
* tree (defensive — e.g. if `_ensureTopLevelContextTracking()` is ever
|
|
1516
|
+
* reached before `initBiDi()`'s callback has run).
|
|
1517
|
+
*/
|
|
1518
|
+
async _startTopLevelContextTracking(initialContexts) {
|
|
1462
1519
|
const conn = this.bidiSession.getConnection();
|
|
1463
|
-
|
|
1464
|
-
.
|
|
1465
|
-
|
|
1520
|
+
if (!initialContexts) {
|
|
1521
|
+
// Defensive path only. In today's sequencing this branch is unreachable:
|
|
1522
|
+
// `initBiDi()` always seeds `_topLevelContextTracking` via `connect()`'s
|
|
1523
|
+
// `onContextTree` callback (with `initialContexts`) before `launch()`
|
|
1524
|
+
// returns, so no external caller can reach `_ensureTopLevelContextTracking()`
|
|
1525
|
+
// — and thus this arg-less path — before `connect()` has already subscribed.
|
|
1526
|
+
// The `subscribe()` is kept (unlike the redundant ones dropped from the
|
|
1527
|
+
// other context-tracking call sites) precisely because this branch's
|
|
1528
|
+
// contract is "connect()'s subscribe may NOT have run yet"; dropping it
|
|
1529
|
+
// would break that contract if a future refactor made the branch reachable.
|
|
1530
|
+
await conn
|
|
1531
|
+
.subscribe(['browsingContext.contextCreated', 'browsingContext.contextDestroyed'])
|
|
1532
|
+
.catch(() => { });
|
|
1533
|
+
}
|
|
1466
1534
|
const onCreated = (params) => {
|
|
1467
1535
|
if (params.parent)
|
|
1468
1536
|
return;
|
|
@@ -1482,7 +1550,17 @@ export class Browser {
|
|
|
1482
1550
|
};
|
|
1483
1551
|
this._topLevelContextTrackingOffs.push(conn.on('browsingContext.contextCreated', onCreated));
|
|
1484
1552
|
this._topLevelContextTrackingOffs.push(conn.on('browsingContext.contextDestroyed', onDestroyed));
|
|
1485
|
-
|
|
1553
|
+
if (initialContexts) {
|
|
1554
|
+
for (const ctx of initialContexts) {
|
|
1555
|
+
if (!ctx.parent) {
|
|
1556
|
+
this._topLevelContextUserContexts.set(ctx.context, ctx.userContext ?? 'default');
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
this._topLevelContextCacheVersion++;
|
|
1560
|
+
}
|
|
1561
|
+
else {
|
|
1562
|
+
await this._refreshTopLevelContextCache(conn).catch(() => { });
|
|
1563
|
+
}
|
|
1486
1564
|
}
|
|
1487
1565
|
async _refreshTopLevelContextCache(conn) {
|
|
1488
1566
|
const version = this._topLevelContextCacheVersion;
|
|
@@ -1551,8 +1629,8 @@ export class Browser {
|
|
|
1551
1629
|
if (this._tracer?.isRunning)
|
|
1552
1630
|
this._tracer.recordAction('click', undefined, selectorToString(selector));
|
|
1553
1631
|
const by = typeof selector === 'string' ? By.css(selector) : selector;
|
|
1554
|
-
const
|
|
1555
|
-
await
|
|
1632
|
+
const timeout = opts?.timeout ?? this.defaults.timeout;
|
|
1633
|
+
await clickWithFastPath(() => this.driver.findElement(by), (remaining) => this.driver.wait(until.elementIsVisible(by), { timeout: remaining }), timeout);
|
|
1556
1634
|
}
|
|
1557
1635
|
async fill(selector, text, opts) {
|
|
1558
1636
|
if (this._tracer?.isRunning)
|