browser-commander 0.26.3 → 0.27.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/.jscpd-baseline.json +24 -23
- package/CHANGELOG.md +6 -0
- package/README.md +2 -0
- package/package.json +5 -3
- package/src/bindings.js +19 -0
- package/src/browser/attach/snapshot-launch.js +8 -6
- package/src/browser/attach/snapshot.js +39 -2
- package/src/browser/browser-cookie-database.js +71 -2
- package/src/browser/browser-cookie-session.js +183 -0
- package/src/browser/browser-cookies.js +29 -0
- package/src/browser/debugging-port.js +4 -2
- package/src/browser/default-browser.js +33 -9
- package/src/browser/launch-diagnostics.js +102 -0
- package/src/browser/launcher.js +28 -8
- package/src/browser/migration/sqlite-snapshot.js +5 -27
- package/src/browser/navigation.js +173 -98
- package/src/browser/real-browser.js +135 -68
- package/src/browser/session-cookies.js +78 -0
- package/src/browser/session-persistence.js +72 -0
- package/src/browser/storage-state.js +32 -6
- package/src/core/dialog-manager.js +7 -0
- package/src/core/managed-navigation.js +101 -0
- package/src/core/navigation-manager.js +62 -78
- package/src/core/navigation-operation.js +25 -0
- package/src/core/navigation-readiness.js +40 -16
- package/src/core/network-tracker.js +39 -92
- package/src/core/page-session.js +3 -2
- package/src/core/page-trigger-manager.js +15 -12
- package/src/core/readiness.js +67 -13
- package/src/core/subscriptions.js +20 -0
- package/src/core/webdriver-page.js +38 -0
- package/src/elements/locators.js +35 -5
- package/src/elements/reusable.js +92 -0
- package/src/elements/visibility.js +6 -11
- package/src/exports.js +14 -0
- package/src/factory.js +4 -4
- package/src/high-level/universal-logic.js +57 -21
- package/src/interactions/click.js +3 -0
- package/src/interactions/scroll.js +8 -2
- package/src/utilities/subprocess.js +15 -4
- package/tests/e2e/navigation-budget.e2e.test.js +132 -0
- package/tests/e2e/reusable-session.e2e.test.js +231 -0
- package/tests/unit/browser/attach/snapshot-launch.test.js +2 -2
- package/tests/unit/browser/attach/snapshot.test.js +33 -0
- package/tests/unit/browser/default-browser.test.js +18 -0
- package/tests/unit/browser/launch-diagnostics.test.js +52 -0
- package/tests/unit/browser/real-browser.test.js +1 -1
- package/tests/unit/browser/session-workflows.test.js +206 -0
- package/tests/unit/browser/storage-state.test.js +7 -2
- package/tests/unit/core/navigation-budget.test.js +120 -0
- package/tests/unit/core/navigation-manager.test.js +23 -1
- package/tests/unit/core/webdriver-page.test.js +33 -0
- package/tests/unit/elements/reusable-helpers.test.js +83 -0
- package/tests/unit/traces/links.test.js +2 -2
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* the NavigationManager for backwards compatibility.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { navigationPhase } from '../core/navigation-operation.js';
|
|
8
9
|
import { TIMING } from '../core/constants.js';
|
|
9
10
|
import { isNavigationError } from '../core/navigation-safety.js';
|
|
10
11
|
import { isActionStoppedError } from '../core/page-trigger-manager.js';
|
|
@@ -13,6 +14,8 @@ import {
|
|
|
13
14
|
createDeadline,
|
|
14
15
|
networkIdleFor,
|
|
15
16
|
runReadinessChecks,
|
|
17
|
+
runWithinDeadline,
|
|
18
|
+
sleepWithinDeadline,
|
|
16
19
|
urlStableFor,
|
|
17
20
|
} from '../core/readiness.js';
|
|
18
21
|
|
|
@@ -102,17 +105,30 @@ export async function verifyNavigation(options = {}) {
|
|
|
102
105
|
log = { debug: () => {} },
|
|
103
106
|
} = options;
|
|
104
107
|
|
|
105
|
-
const
|
|
108
|
+
const deadline =
|
|
109
|
+
options.deadline ?? createDeadline({ timeout, signal: options.signal });
|
|
110
|
+
const verificationEnd = performance.now() + timeout;
|
|
111
|
+
const budget = {
|
|
112
|
+
...deadline,
|
|
113
|
+
remainingMs: () =>
|
|
114
|
+
Math.max(
|
|
115
|
+
0,
|
|
116
|
+
Math.min(deadline.remainingMs(), verificationEnd - performance.now())
|
|
117
|
+
),
|
|
118
|
+
expired: () => deadline.expired() || performance.now() >= verificationEnd,
|
|
119
|
+
};
|
|
106
120
|
let attempts = 0;
|
|
107
121
|
let lastResult = { verified: false, actualUrl: '', reason: '' };
|
|
108
122
|
|
|
109
|
-
while (
|
|
123
|
+
while (!budget.expired() && !budget.signal?.aborted) {
|
|
110
124
|
attempts++;
|
|
111
|
-
|
|
112
|
-
page,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
125
|
+
const probe = await runWithinDeadline(budget, () =>
|
|
126
|
+
verifyFn({ page, expectedUrl, startUrl })
|
|
127
|
+
);
|
|
128
|
+
if (probe.timedOut || probe.interrupted) {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
lastResult = probe.value;
|
|
116
132
|
|
|
117
133
|
if (lastResult.verified) {
|
|
118
134
|
log.debug(
|
|
@@ -128,7 +144,7 @@ export async function verifyNavigation(options = {}) {
|
|
|
128
144
|
}
|
|
129
145
|
|
|
130
146
|
// Wait before next retry
|
|
131
|
-
await
|
|
147
|
+
await sleepWithinDeadline(retryInterval, budget);
|
|
132
148
|
}
|
|
133
149
|
|
|
134
150
|
log.debug(
|
|
@@ -158,7 +174,7 @@ export async function waitForUrlStabilization(options = {}) {
|
|
|
158
174
|
const {
|
|
159
175
|
page,
|
|
160
176
|
log,
|
|
161
|
-
wait,
|
|
177
|
+
wait: _wait,
|
|
162
178
|
navigationManager,
|
|
163
179
|
stableChecks = 3,
|
|
164
180
|
checkInterval = 1000,
|
|
@@ -168,25 +184,29 @@ export async function waitForUrlStabilization(options = {}) {
|
|
|
168
184
|
|
|
169
185
|
// If NavigationManager is available, delegate to it
|
|
170
186
|
if (navigationManager) {
|
|
171
|
-
return navigationManager.waitForPageReady({ timeout, reason });
|
|
187
|
+
return navigationManager.waitForPageReady({ ...options, timeout, reason });
|
|
172
188
|
}
|
|
173
189
|
|
|
174
190
|
// Legacy polling-based approach
|
|
175
191
|
log.debug(() => `⏳ Waiting for URL to stabilize (${reason})...`);
|
|
176
192
|
let stableCount = 0;
|
|
177
193
|
let lastUrl = page.url();
|
|
178
|
-
const
|
|
194
|
+
const deadline =
|
|
195
|
+
options.deadline ?? createDeadline({ timeout, signal: options.signal });
|
|
179
196
|
|
|
180
197
|
while (stableCount < stableChecks) {
|
|
181
198
|
// Check timeout
|
|
182
|
-
if (
|
|
199
|
+
if (deadline.expired() || deadline.signal?.aborted) {
|
|
183
200
|
log.debug(
|
|
184
201
|
() => `⚠️ URL stabilization timeout after ${timeout}ms (${reason})`
|
|
185
202
|
);
|
|
186
203
|
return false;
|
|
187
204
|
}
|
|
188
205
|
|
|
189
|
-
await
|
|
206
|
+
await sleepWithinDeadline(checkInterval, deadline);
|
|
207
|
+
if (deadline.expired() || deadline.signal?.aborted) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
190
210
|
const currentUrl = page.url();
|
|
191
211
|
|
|
192
212
|
if (currentUrl === lastUrl) {
|
|
@@ -209,6 +229,70 @@ export async function waitForUrlStabilization(options = {}) {
|
|
|
209
229
|
return true;
|
|
210
230
|
}
|
|
211
231
|
|
|
232
|
+
async function legacyGotoReadiness(options, deadline) {
|
|
233
|
+
const {
|
|
234
|
+
page,
|
|
235
|
+
url,
|
|
236
|
+
waitForUrlStabilization: stabilizeFn,
|
|
237
|
+
waitUntil,
|
|
238
|
+
waitForStableUrlBefore,
|
|
239
|
+
waitForStableUrlAfter,
|
|
240
|
+
waitForNetworkIdle,
|
|
241
|
+
stableChecks,
|
|
242
|
+
checkInterval,
|
|
243
|
+
checks,
|
|
244
|
+
} = options;
|
|
245
|
+
if (waitForStableUrlBefore && stabilizeFn) {
|
|
246
|
+
await navigationPhase(deadline, () =>
|
|
247
|
+
stabilizeFn({
|
|
248
|
+
stableChecks,
|
|
249
|
+
checkInterval,
|
|
250
|
+
timeout: deadline.remainingMs(),
|
|
251
|
+
deadline,
|
|
252
|
+
reason: 'before navigation',
|
|
253
|
+
})
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
await navigationPhase(deadline, () =>
|
|
257
|
+
page.goto(url, {
|
|
258
|
+
waitUntil,
|
|
259
|
+
timeout: Math.max(1, deadline.remainingMs()),
|
|
260
|
+
})
|
|
261
|
+
);
|
|
262
|
+
const readiness = checks ?? [
|
|
263
|
+
...(waitForStableUrlAfter
|
|
264
|
+
? [
|
|
265
|
+
urlStableFor({
|
|
266
|
+
intervalMs: checkInterval,
|
|
267
|
+
stableForMs: checkInterval,
|
|
268
|
+
consecutiveSamples: stableChecks,
|
|
269
|
+
}),
|
|
270
|
+
]
|
|
271
|
+
: []),
|
|
272
|
+
...(waitForNetworkIdle ? [networkIdleFor()] : []),
|
|
273
|
+
];
|
|
274
|
+
return runReadinessChecks({
|
|
275
|
+
checks: readiness,
|
|
276
|
+
deadline,
|
|
277
|
+
context: { page, networkTracker: options.networkTracker },
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function navigationFailureStatus(error, signal) {
|
|
282
|
+
if (signal?.aborted) {
|
|
283
|
+
return 'interrupted';
|
|
284
|
+
}
|
|
285
|
+
if (error.status) {
|
|
286
|
+
return error.status;
|
|
287
|
+
}
|
|
288
|
+
if (error.name === 'TimeoutError') {
|
|
289
|
+
return 'timed_out';
|
|
290
|
+
}
|
|
291
|
+
return isNavigationError(error) || isActionStoppedError(error)
|
|
292
|
+
? 'interrupted'
|
|
293
|
+
: null;
|
|
294
|
+
}
|
|
295
|
+
|
|
212
296
|
/**
|
|
213
297
|
* Navigate to URL with full wait for page ready
|
|
214
298
|
* @param {Object} options - Configuration options
|
|
@@ -232,14 +316,13 @@ export async function waitForUrlStabilization(options = {}) {
|
|
|
232
316
|
export async function goto(options = {}) {
|
|
233
317
|
const {
|
|
234
318
|
page,
|
|
235
|
-
waitForUrlStabilization: stabilizeFn,
|
|
236
319
|
navigationManager,
|
|
237
320
|
log = { debug: () => {} },
|
|
238
321
|
url,
|
|
239
322
|
waitUntil = 'domcontentloaded',
|
|
240
323
|
waitForStableUrlBefore = true,
|
|
241
324
|
waitForStableUrlAfter = true,
|
|
242
|
-
waitForNetworkIdle
|
|
325
|
+
waitForNetworkIdle = true,
|
|
243
326
|
stableChecks = 3,
|
|
244
327
|
checkInterval = 1000,
|
|
245
328
|
timeout = 240000,
|
|
@@ -252,103 +335,95 @@ export async function goto(options = {}) {
|
|
|
252
335
|
throw new Error('url is required in options');
|
|
253
336
|
}
|
|
254
337
|
|
|
338
|
+
const { signal, checks } = options;
|
|
339
|
+
const deadline = options.deadline ?? createDeadline({ timeout, signal });
|
|
255
340
|
const startUrl = page.url();
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
341
|
+
try {
|
|
342
|
+
let outcome;
|
|
343
|
+
if (navigationManager) {
|
|
344
|
+
const result = await navigationPhase(deadline, () =>
|
|
345
|
+
navigationManager.navigate({
|
|
346
|
+
url,
|
|
347
|
+
waitUntil,
|
|
348
|
+
timeout,
|
|
349
|
+
deadline,
|
|
350
|
+
signal,
|
|
351
|
+
checks,
|
|
352
|
+
waitForStableUrlBefore,
|
|
353
|
+
waitForStableUrlAfter,
|
|
354
|
+
waitForNetworkIdle,
|
|
355
|
+
stableChecks,
|
|
356
|
+
checkInterval,
|
|
357
|
+
returnOutcome: true,
|
|
358
|
+
})
|
|
359
|
+
);
|
|
360
|
+
outcome =
|
|
361
|
+
typeof result === 'boolean'
|
|
362
|
+
? { ready: result, status: result ? 'ready' : 'failed' }
|
|
363
|
+
: result;
|
|
364
|
+
} else {
|
|
365
|
+
outcome = await legacyGotoReadiness(
|
|
366
|
+
{
|
|
367
|
+
...options,
|
|
368
|
+
waitUntil,
|
|
369
|
+
waitForStableUrlBefore,
|
|
370
|
+
waitForStableUrlAfter,
|
|
371
|
+
waitForNetworkIdle,
|
|
372
|
+
stableChecks,
|
|
373
|
+
checkInterval,
|
|
374
|
+
},
|
|
375
|
+
deadline
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (!outcome.ready) {
|
|
379
|
+
return {
|
|
380
|
+
navigated: false,
|
|
381
|
+
verified: false,
|
|
382
|
+
actualUrl: page.url(),
|
|
383
|
+
status: outcome.status,
|
|
384
|
+
readiness: outcome,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (verify) {
|
|
388
|
+
const verified = await navigationPhase(deadline, () =>
|
|
389
|
+
verifyNavigation({
|
|
269
390
|
page,
|
|
270
391
|
expectedUrl: url,
|
|
271
392
|
startUrl,
|
|
272
393
|
verifyFn,
|
|
273
|
-
timeout: verificationTimeout,
|
|
394
|
+
timeout: Math.min(verificationTimeout, deadline.remainingMs()),
|
|
274
395
|
log,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return { navigated, verified: navigated, actualUrl: page.url() };
|
|
286
|
-
} catch (error) {
|
|
287
|
-
if (isNavigationError(error) || isActionStoppedError(error)) {
|
|
288
|
-
// Navigation was stopped by page trigger or navigation error
|
|
289
|
-
// This is not a failure - it means another action took over
|
|
290
|
-
return {
|
|
291
|
-
navigated: false,
|
|
292
|
-
verified: false,
|
|
293
|
-
reason: 'navigation stopped/interrupted',
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
throw error;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Legacy approach without NavigationManager
|
|
301
|
-
try {
|
|
302
|
-
// Wait for URL to stabilize BEFORE navigation (to avoid interrupting natural redirects)
|
|
303
|
-
if (waitForStableUrlBefore && stabilizeFn) {
|
|
304
|
-
await stabilizeFn({
|
|
305
|
-
stableChecks,
|
|
306
|
-
checkInterval,
|
|
307
|
-
reason: 'before navigation',
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Navigate to the URL
|
|
312
|
-
await page.goto(url, { waitUntil, timeout });
|
|
313
|
-
|
|
314
|
-
// Wait for URL to stabilize AFTER navigation (to ensure all redirects are complete)
|
|
315
|
-
if (waitForStableUrlAfter && stabilizeFn) {
|
|
316
|
-
await stabilizeFn({
|
|
317
|
-
stableChecks,
|
|
318
|
-
checkInterval,
|
|
319
|
-
reason: 'after navigation',
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Verify navigation if requested
|
|
324
|
-
if (verify) {
|
|
325
|
-
const verificationResult = await verifyNavigation({
|
|
326
|
-
page,
|
|
327
|
-
expectedUrl: url,
|
|
328
|
-
startUrl,
|
|
329
|
-
verifyFn,
|
|
330
|
-
timeout: verificationTimeout,
|
|
331
|
-
log,
|
|
332
|
-
});
|
|
333
|
-
|
|
396
|
+
deadline: {
|
|
397
|
+
...deadline,
|
|
398
|
+
remainingMs: () =>
|
|
399
|
+
Math.min(verificationTimeout, deadline.remainingMs()),
|
|
400
|
+
},
|
|
401
|
+
})
|
|
402
|
+
);
|
|
334
403
|
return {
|
|
335
404
|
navigated: true,
|
|
336
|
-
verified
|
|
337
|
-
|
|
338
|
-
|
|
405
|
+
...verified,
|
|
406
|
+
status: verified.verified ? 'ready' : 'failed',
|
|
407
|
+
readiness: outcome,
|
|
339
408
|
};
|
|
340
409
|
}
|
|
341
|
-
|
|
342
|
-
|
|
410
|
+
return {
|
|
411
|
+
navigated: true,
|
|
412
|
+
verified: true,
|
|
413
|
+
actualUrl: page.url(),
|
|
414
|
+
status: 'ready',
|
|
415
|
+
readiness: outcome,
|
|
416
|
+
};
|
|
343
417
|
} catch (error) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
);
|
|
418
|
+
const status = navigationFailureStatus(error, signal);
|
|
419
|
+
if (status) {
|
|
420
|
+
navigationManager?.cancelNavigation?.();
|
|
348
421
|
return {
|
|
349
422
|
navigated: false,
|
|
350
423
|
verified: false,
|
|
351
|
-
|
|
424
|
+
actualUrl: page.url(),
|
|
425
|
+
status,
|
|
426
|
+
reason: error.message,
|
|
352
427
|
};
|
|
353
428
|
}
|
|
354
429
|
throw error;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { launchFailure, redactLaunchEvidence } from './launch-diagnostics.js';
|
|
2
|
+
import {
|
|
3
|
+
installSessionPersistence,
|
|
4
|
+
sessionPersistencePath,
|
|
5
|
+
} from './session-persistence.js';
|
|
1
6
|
import { readFile } from 'node:fs/promises';
|
|
2
7
|
import path from 'node:path';
|
|
3
8
|
import { findBrowserSource } from './browser-sources.js';
|
|
@@ -187,56 +192,66 @@ export async function waitForCdpEndpoint({
|
|
|
187
192
|
timeout = 30_000,
|
|
188
193
|
fetchImplementation = globalThis.fetch,
|
|
189
194
|
}) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (browserProcess.exitCode !== null) {
|
|
202
|
-
throw new Error(
|
|
203
|
-
`Browser exited before its DevTools endpoint was ready (exit ${browserProcess.exitCode})`
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
if (ownership !== 'owned') {
|
|
207
|
-
const activePort = await readDevToolsActivePort(userDataDir);
|
|
208
|
-
if (activePort?.port === port) {
|
|
209
|
-
ownership = 'owned';
|
|
195
|
+
try {
|
|
196
|
+
const port = assertFixedDebuggingPort(remoteDebuggingPort);
|
|
197
|
+
const endpoint = `http://${LOOPBACK_HOST}:${port}`;
|
|
198
|
+
const deadline = Date.now() + timeout;
|
|
199
|
+
while (Date.now() < deadline) {
|
|
200
|
+
const output = devToolsOutput.state();
|
|
201
|
+
let ownership = devToolsOutput.available
|
|
202
|
+
? classifyDevToolsOwnership(output, port)
|
|
203
|
+
: 'unknown';
|
|
204
|
+
if (ownership === 'race') {
|
|
205
|
+
throw new PortRaceError(port, output.listening?.url ?? 'bind failed');
|
|
210
206
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
207
|
+
if (browserProcess.exitCode !== null) {
|
|
208
|
+
throw (
|
|
209
|
+
browserProcess.spawnError ??
|
|
210
|
+
Object.assign(
|
|
211
|
+
new Error(
|
|
212
|
+
`Browser exited before its DevTools endpoint was ready (exit ${browserProcess.exitCode})`
|
|
213
|
+
),
|
|
214
|
+
{ stderrTail: browserProcess.stderrTail }
|
|
215
|
+
)
|
|
220
216
|
);
|
|
221
|
-
} catch {
|
|
222
|
-
// The HTTP handler can lag the listening line by a moment.
|
|
223
217
|
}
|
|
224
|
-
if (
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
218
|
+
if (ownership !== 'owned') {
|
|
219
|
+
const activePort = await readDevToolsActivePort(userDataDir);
|
|
220
|
+
if (activePort?.port === port) {
|
|
221
|
+
ownership = 'owned';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (ownership === 'owned') {
|
|
226
|
+
let version = null;
|
|
227
|
+
try {
|
|
228
|
+
version = await fetchCdpVersion(
|
|
229
|
+
endpoint,
|
|
230
|
+
fetchImplementation,
|
|
231
|
+
Math.max(1, deadline - Date.now())
|
|
230
232
|
);
|
|
233
|
+
} catch {
|
|
234
|
+
// The HTTP handler can lag the listening line by a moment.
|
|
235
|
+
}
|
|
236
|
+
if (version) {
|
|
237
|
+
const expected = output.listening?.url;
|
|
238
|
+
if (expected && version.webSocketDebuggerUrl !== expected) {
|
|
239
|
+
throw new PortRaceError(
|
|
240
|
+
port,
|
|
241
|
+
`port serves ${version.webSocketDebuggerUrl}, browser announced ${expected}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return endpoint;
|
|
231
245
|
}
|
|
232
|
-
return endpoint;
|
|
233
246
|
}
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
234
248
|
}
|
|
235
|
-
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Timed out after ${timeout}ms waiting for the DevTools endpoint on port ${port}`
|
|
251
|
+
);
|
|
252
|
+
} finally {
|
|
253
|
+
devToolsOutput.dispose?.();
|
|
236
254
|
}
|
|
237
|
-
throw new Error(
|
|
238
|
-
`Timed out after ${timeout}ms waiting for the DevTools endpoint on port ${port}`
|
|
239
|
-
);
|
|
240
255
|
}
|
|
241
256
|
|
|
242
257
|
function spawnWithStartProcess(executablePath, args, { env, verbose }) {
|
|
@@ -251,13 +266,19 @@ async function waitForExit(browserProcess, timeout) {
|
|
|
251
266
|
return false;
|
|
252
267
|
}
|
|
253
268
|
let timer;
|
|
269
|
+
const onExit = () => resolveExit(true);
|
|
270
|
+
let resolveExit;
|
|
254
271
|
const exited = await Promise.race([
|
|
255
|
-
new Promise((resolve) =>
|
|
272
|
+
new Promise((resolve) => {
|
|
273
|
+
resolveExit = resolve;
|
|
274
|
+
browserProcess.once('exit', onExit);
|
|
275
|
+
}),
|
|
256
276
|
new Promise((resolve) => {
|
|
257
277
|
timer = setTimeout(() => resolve(false), timeout);
|
|
258
278
|
}),
|
|
259
279
|
]);
|
|
260
280
|
clearTimeout(timer);
|
|
281
|
+
browserProcess.removeListener?.('exit', onExit);
|
|
261
282
|
return exited;
|
|
262
283
|
}
|
|
263
284
|
|
|
@@ -369,6 +390,7 @@ async function spawnOnFreePort({
|
|
|
369
390
|
userDataDir,
|
|
370
391
|
startupTimeout,
|
|
371
392
|
dependencies,
|
|
393
|
+
diagnostics,
|
|
372
394
|
}) {
|
|
373
395
|
const reservePort = dependencies.reservePort ?? reserveLoopbackPort;
|
|
374
396
|
const spawnBrowser = dependencies.spawnBrowser ?? spawnWithStartProcess;
|
|
@@ -381,10 +403,20 @@ async function spawnOnFreePort({
|
|
|
381
403
|
userDataDir,
|
|
382
404
|
remoteDebuggingPort: port,
|
|
383
405
|
});
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
406
|
+
let browserProcess;
|
|
407
|
+
try {
|
|
408
|
+
browserProcess = await spawnBrowser(executablePath, browserArgs, {
|
|
409
|
+
env,
|
|
410
|
+
verbose: false,
|
|
411
|
+
});
|
|
412
|
+
} catch (error) {
|
|
413
|
+
throw launchFailure(error, {
|
|
414
|
+
...diagnostics,
|
|
415
|
+
phase: 'spawn',
|
|
416
|
+
executablePath,
|
|
417
|
+
userDataDir,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
388
420
|
try {
|
|
389
421
|
const cdpEndpoint = await waitForEndpoint({
|
|
390
422
|
remoteDebuggingPort: port,
|
|
@@ -397,13 +429,24 @@ async function spawnOnFreePort({
|
|
|
397
429
|
if (browserProcess.exitCode === null) {
|
|
398
430
|
browserProcess.kill();
|
|
399
431
|
}
|
|
432
|
+
await waitForExit(browserProcess, 5_000);
|
|
400
433
|
if (!(error instanceof PortRaceError) || attempt >= attempts) {
|
|
401
|
-
throw error
|
|
434
|
+
throw launchFailure(error, {
|
|
435
|
+
...diagnostics,
|
|
436
|
+
phase: 'endpoint',
|
|
437
|
+
browserProcess,
|
|
438
|
+
executablePath,
|
|
439
|
+
userDataDir,
|
|
440
|
+
});
|
|
402
441
|
}
|
|
403
442
|
if (verbose) {
|
|
404
|
-
console.log(
|
|
443
|
+
console.log(
|
|
444
|
+
redactLaunchEvidence(
|
|
445
|
+
`${error.message}; retrying with a new port`,
|
|
446
|
+
diagnostics?.diagnosticRedactor
|
|
447
|
+
)
|
|
448
|
+
);
|
|
405
449
|
}
|
|
406
|
-
await waitForExit(browserProcess, 5_000);
|
|
407
450
|
}
|
|
408
451
|
}
|
|
409
452
|
}
|
|
@@ -565,6 +608,7 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
565
608
|
options = {},
|
|
566
609
|
dependencies = {}
|
|
567
610
|
) {
|
|
611
|
+
sessionPersistencePath(options);
|
|
568
612
|
if (isSafariChannel(options.channel ?? options.browser)) {
|
|
569
613
|
return launchSafari(options, dependencies);
|
|
570
614
|
}
|
|
@@ -614,10 +658,22 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
614
658
|
|
|
615
659
|
const resolveExecutable =
|
|
616
660
|
dependencies.resolveExecutable ?? resolveSystemBrowserExecutable;
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
661
|
+
const launchChannel = attach?.browser ?? channel;
|
|
662
|
+
const executablePath = await Promise.resolve()
|
|
663
|
+
.then(() =>
|
|
664
|
+
resolveExecutable({
|
|
665
|
+
channel: launchChannel,
|
|
666
|
+
executablePath: requestedExecutablePath,
|
|
667
|
+
})
|
|
668
|
+
)
|
|
669
|
+
.catch((error) => {
|
|
670
|
+
throw launchFailure(error, {
|
|
671
|
+
...options,
|
|
672
|
+
phase: 'discovery',
|
|
673
|
+
engine,
|
|
674
|
+
executablePath: requestedExecutablePath,
|
|
675
|
+
});
|
|
676
|
+
});
|
|
621
677
|
|
|
622
678
|
const temporaryProfile = !requestedUserDataDir;
|
|
623
679
|
const { userDataDir, snapshotLaunch } = await prepareProfile({
|
|
@@ -661,6 +717,7 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
661
717
|
userDataDir,
|
|
662
718
|
startupTimeout,
|
|
663
719
|
dependencies,
|
|
720
|
+
diagnostics: { engine, diagnosticRedactor: options.diagnosticRedactor },
|
|
664
721
|
})
|
|
665
722
|
);
|
|
666
723
|
const { browserProcess, cdpEndpoint: resolvedCdpEndpoint } = launched;
|
|
@@ -694,19 +751,22 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
694
751
|
closeTimeout,
|
|
695
752
|
closeConnection: engine === 'selenium' ? connection.close : undefined,
|
|
696
753
|
});
|
|
697
|
-
return
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
754
|
+
return await installSessionPersistence(
|
|
755
|
+
{
|
|
756
|
+
...connection,
|
|
757
|
+
close,
|
|
758
|
+
browserProcess,
|
|
759
|
+
cdpEndpoint: resolvedCdpEndpoint,
|
|
760
|
+
remoteDebuggingPort: launched.port,
|
|
761
|
+
executablePath,
|
|
762
|
+
userDataDir,
|
|
763
|
+
temporaryProfile,
|
|
764
|
+
args: launched.browserArgs,
|
|
765
|
+
...(migration ? { migration } : {}),
|
|
766
|
+
...(snapshotLaunch ? { attach: snapshotLaunch.attach } : {}),
|
|
767
|
+
},
|
|
768
|
+
options
|
|
769
|
+
);
|
|
710
770
|
} catch (error) {
|
|
711
771
|
if (browserProcess.exitCode === null) {
|
|
712
772
|
browserProcess.kill();
|
|
@@ -715,7 +775,14 @@ export async function launchAndConnectRealBrowserWithDependencies(
|
|
|
715
775
|
if (temporaryProfile) {
|
|
716
776
|
await removeUserDataDir(userDataDir);
|
|
717
777
|
}
|
|
718
|
-
throw error
|
|
778
|
+
throw launchFailure(error, {
|
|
779
|
+
...options,
|
|
780
|
+
phase: 'connect',
|
|
781
|
+
engine,
|
|
782
|
+
browserProcess,
|
|
783
|
+
executablePath,
|
|
784
|
+
userDataDir,
|
|
785
|
+
});
|
|
719
786
|
}
|
|
720
787
|
}
|
|
721
788
|
|