browser-commander 0.26.2 → 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.
Files changed (58) hide show
  1. package/.jscpd-baseline.json +24 -23
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +2 -0
  4. package/package.json +5 -3
  5. package/src/bindings.js +19 -0
  6. package/src/browser/attach/snapshot-launch.js +8 -6
  7. package/src/browser/attach/snapshot.js +39 -2
  8. package/src/browser/browser-cookie-database.js +71 -2
  9. package/src/browser/browser-cookie-session.js +183 -0
  10. package/src/browser/browser-cookies.js +29 -0
  11. package/src/browser/browser-sources.js +13 -0
  12. package/src/browser/browser-sources.json +13 -0
  13. package/src/browser/debugging-port.js +4 -2
  14. package/src/browser/default-browser.js +33 -9
  15. package/src/browser/launch-diagnostics.js +102 -0
  16. package/src/browser/launcher.js +28 -8
  17. package/src/browser/migration/sqlite-snapshot.js +5 -27
  18. package/src/browser/navigation.js +173 -98
  19. package/src/browser/real-browser.js +135 -68
  20. package/src/browser/session-cookies.js +78 -0
  21. package/src/browser/session-persistence.js +72 -0
  22. package/src/browser/storage-state.js +32 -6
  23. package/src/browser/system-browser.js +2 -2
  24. package/src/core/dialog-manager.js +7 -0
  25. package/src/core/managed-navigation.js +101 -0
  26. package/src/core/navigation-manager.js +62 -78
  27. package/src/core/navigation-operation.js +25 -0
  28. package/src/core/navigation-readiness.js +40 -16
  29. package/src/core/network-tracker.js +39 -92
  30. package/src/core/page-session.js +3 -2
  31. package/src/core/page-trigger-manager.js +15 -12
  32. package/src/core/readiness.js +67 -13
  33. package/src/core/subscriptions.js +20 -0
  34. package/src/core/webdriver-page.js +38 -0
  35. package/src/elements/locators.js +35 -5
  36. package/src/elements/reusable.js +92 -0
  37. package/src/elements/visibility.js +6 -11
  38. package/src/exports.js +14 -0
  39. package/src/factory.js +4 -4
  40. package/src/high-level/universal-logic.js +57 -21
  41. package/src/interactions/click.js +3 -0
  42. package/src/interactions/scroll.js +8 -2
  43. package/src/utilities/subprocess.js +15 -4
  44. package/tests/e2e/navigation-budget.e2e.test.js +132 -0
  45. package/tests/e2e/reusable-session.e2e.test.js +231 -0
  46. package/tests/unit/browser/attach/snapshot-launch.test.js +2 -2
  47. package/tests/unit/browser/attach/snapshot.test.js +33 -0
  48. package/tests/unit/browser/catalogue-launch.test.js +50 -2
  49. package/tests/unit/browser/default-browser.test.js +18 -0
  50. package/tests/unit/browser/launch-diagnostics.test.js +52 -0
  51. package/tests/unit/browser/real-browser.test.js +1 -1
  52. package/tests/unit/browser/session-workflows.test.js +206 -0
  53. package/tests/unit/browser/storage-state.test.js +7 -2
  54. package/tests/unit/core/navigation-budget.test.js +120 -0
  55. package/tests/unit/core/navigation-manager.test.js +23 -1
  56. package/tests/unit/core/webdriver-page.test.js +33 -0
  57. package/tests/unit/elements/reusable-helpers.test.js +83 -0
  58. 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 startTime = Date.now();
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 (Date.now() - startTime < timeout) {
123
+ while (!budget.expired() && !budget.signal?.aborted) {
110
124
  attempts++;
111
- lastResult = await verifyFn({
112
- page,
113
- expectedUrl,
114
- startUrl,
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 new Promise((resolve) => setTimeout(resolve, retryInterval));
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 startTime = Date.now();
194
+ const deadline =
195
+ options.deadline ?? createDeadline({ timeout, signal: options.signal });
179
196
 
180
197
  while (stableCount < stableChecks) {
181
198
  // Check timeout
182
- if (Date.now() - startTime > timeout) {
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 wait({ ms: checkInterval, reason: 'checking URL stability' });
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: _waitForNetworkIdle = true,
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
- // If NavigationManager is available, use it for full navigation handling
258
- if (navigationManager) {
259
- try {
260
- const navigated = await navigationManager.navigate({
261
- url,
262
- waitUntil,
263
- timeout,
264
- });
265
-
266
- // Verify navigation if requested
267
- if (verify && navigated) {
268
- const verificationResult = await verifyNavigation({
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
- return {
278
- navigated: true,
279
- verified: verificationResult.verified,
280
- actualUrl: verificationResult.actualUrl,
281
- reason: verificationResult.reason,
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: verificationResult.verified,
337
- actualUrl: verificationResult.actualUrl,
338
- reason: verificationResult.reason,
405
+ ...verified,
406
+ status: verified.verified ? 'ready' : 'failed',
407
+ readiness: outcome,
339
408
  };
340
409
  }
341
-
342
- return { navigated: true, verified: true, actualUrl: page.url() };
410
+ return {
411
+ navigated: true,
412
+ verified: true,
413
+ actualUrl: page.url(),
414
+ status: 'ready',
415
+ readiness: outcome,
416
+ };
343
417
  } catch (error) {
344
- if (isNavigationError(error) || isActionStoppedError(error)) {
345
- console.log(
346
- '⚠️ Navigation was interrupted/stopped, recovering gracefully'
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
- reason: 'navigation interrupted/stopped',
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
- const port = assertFixedDebuggingPort(remoteDebuggingPort);
191
- const endpoint = `http://${LOOPBACK_HOST}:${port}`;
192
- const deadline = Date.now() + timeout;
193
- while (Date.now() < deadline) {
194
- const output = devToolsOutput.state();
195
- let ownership = devToolsOutput.available
196
- ? classifyDevToolsOwnership(output, port)
197
- : 'unknown';
198
- if (ownership === 'race') {
199
- throw new PortRaceError(port, output.listening?.url ?? 'bind failed');
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
- if (ownership === 'owned') {
214
- let version = null;
215
- try {
216
- version = await fetchCdpVersion(
217
- endpoint,
218
- fetchImplementation,
219
- Math.max(1, deadline - Date.now())
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 (version) {
225
- const expected = output.listening?.url;
226
- if (expected && version.webSocketDebuggerUrl !== expected) {
227
- throw new PortRaceError(
228
- port,
229
- `port serves ${version.webSocketDebuggerUrl}, browser announced ${expected}`
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
- await new Promise((resolve) => setTimeout(resolve, 100));
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) => browserProcess.once('exit', () => resolve(true))),
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
- const browserProcess = spawnBrowser(executablePath, browserArgs, {
385
- env,
386
- verbose,
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(`${error.message}; retrying with a new port`);
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 executablePath = await resolveExecutable({
618
- channel,
619
- executablePath: requestedExecutablePath,
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
- ...connection,
699
- close,
700
- browserProcess,
701
- cdpEndpoint: resolvedCdpEndpoint,
702
- remoteDebuggingPort: launched.port,
703
- executablePath,
704
- userDataDir,
705
- temporaryProfile,
706
- args: launched.browserArgs,
707
- ...(migration ? { migration } : {}),
708
- ...(snapshotLaunch ? { attach: snapshotLaunch.attach } : {}),
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