browser-commander 0.26.3 → 0.28.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 (115) hide show
  1. package/.jscpd-baseline.json +24 -23
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +6 -0
  4. package/eslint.config.js +11 -0
  5. package/package.json +9 -4
  6. package/src/bindings.js +19 -0
  7. package/src/browser/attach/snapshot-launch.js +8 -6
  8. package/src/browser/attach/snapshot.js +39 -2
  9. package/src/browser/browser-cookie-database.js +71 -2
  10. package/src/browser/browser-cookie-session.js +183 -0
  11. package/src/browser/browser-cookies.js +29 -0
  12. package/src/browser/connector.js +117 -13
  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/launch-options.js +2 -1
  17. package/src/browser/launch-restrictions.json +33 -2
  18. package/src/browser/launcher.js +54 -9
  19. package/src/browser/migration/sqlite-snapshot.js +5 -27
  20. package/src/browser/navigation.js +173 -98
  21. package/src/browser/persistent-session.js +360 -0
  22. package/src/browser/profile-directory.js +20 -0
  23. package/src/browser/real-browser.js +142 -70
  24. package/src/browser/restrictions.js +13 -4
  25. package/src/browser/session-cookies.js +78 -0
  26. package/src/browser/session-persistence.js +72 -0
  27. package/src/browser/session-watchdog.js +71 -0
  28. package/src/browser/session-worker.js +70 -0
  29. package/src/browser/storage-state.js +32 -6
  30. package/src/browser/webdriver.js +4 -0
  31. package/src/capture/encoding.js +327 -0
  32. package/src/capture/errors.js +12 -0
  33. package/src/capture/files.js +9 -0
  34. package/src/capture/index.js +5 -0
  35. package/src/capture/recording.js +136 -0
  36. package/src/capture/screenshot.js +222 -0
  37. package/src/capture/video.js +186 -0
  38. package/src/capture/viewport.js +68 -0
  39. package/src/capture/worker.js +22 -0
  40. package/src/cli/args.js +66 -1
  41. package/src/cli/commands.js +113 -2
  42. package/src/cli/high-level-methods.js +40 -3
  43. package/src/cli/sessions.js +11 -3
  44. package/src/core/commander-features.js +105 -0
  45. package/src/core/constants.js +1 -0
  46. package/src/core/dialog-manager.js +7 -0
  47. package/src/core/managed-navigation.js +101 -0
  48. package/src/core/navigation-manager.js +66 -80
  49. package/src/core/navigation-operation.js +25 -0
  50. package/src/core/navigation-readiness.js +68 -16
  51. package/src/core/navigation-safety.js +51 -0
  52. package/src/core/network-tracker.js +39 -92
  53. package/src/core/page-session.js +3 -2
  54. package/src/core/page-trigger-manager.js +130 -42
  55. package/src/core/readiness.js +76 -14
  56. package/src/core/subscriptions.js +20 -0
  57. package/src/core/webdriver-page.js +38 -0
  58. package/src/elements/locators.js +61 -37
  59. package/src/elements/reusable.js +92 -0
  60. package/src/elements/selectors.js +37 -81
  61. package/src/elements/visibility.js +38 -21
  62. package/src/exports.js +14 -0
  63. package/src/factory.js +17 -16
  64. package/src/high-level/universal-logic.js +57 -21
  65. package/src/index.js +2 -0
  66. package/src/interactions/click.js +47 -0
  67. package/src/interactions/overlays.js +28 -0
  68. package/src/interactions/scroll.js +8 -2
  69. package/src/traces/bundle.js +14 -5
  70. package/src/traces/checkpoints.js +99 -0
  71. package/src/traces/links.js +117 -1
  72. package/src/traces/mutation-stream.js +1 -0
  73. package/src/traces/network.js +257 -0
  74. package/src/traces/page-capture.js +143 -52
  75. package/src/traces/reader.js +8 -10
  76. package/src/traces/recorder.js +221 -245
  77. package/src/traces/render.js +168 -0
  78. package/src/traces/rolling.js +180 -0
  79. package/src/traces/start-cleanup.js +15 -0
  80. package/src/traces/stop.js +159 -0
  81. package/src/traces/storage.js +43 -0
  82. package/src/utilities/subprocess.js +47 -4
  83. package/tests/e2e/cli.e2e.test.js +20 -1
  84. package/tests/e2e/click-readiness.e2e.test.js +1 -1
  85. package/tests/e2e/navigation-budget.e2e.test.js +132 -0
  86. package/tests/e2e/reusable-session.e2e.test.js +231 -0
  87. package/tests/unit/browser/attach/snapshot-launch.test.js +2 -2
  88. package/tests/unit/browser/attach/snapshot.test.js +33 -0
  89. package/tests/unit/browser/default-browser.test.js +18 -0
  90. package/tests/unit/browser/launch-building-blocks.test.js +5 -1
  91. package/tests/unit/browser/launch-diagnostics.test.js +52 -0
  92. package/tests/unit/browser/launcher.test.js +5 -1
  93. package/tests/unit/browser/persistent-session.test.js +59 -0
  94. package/tests/unit/browser/quiet-ui.test.js +16 -0
  95. package/tests/unit/browser/real-browser.test.js +1 -1
  96. package/tests/unit/browser/session-workflows.test.js +206 -0
  97. package/tests/unit/browser/storage-state.test.js +7 -2
  98. package/tests/unit/browser/tab-selection.test.js +38 -0
  99. package/tests/unit/capture/capture.test.js +52 -0
  100. package/tests/unit/capture/cleanup.test.js +24 -0
  101. package/tests/unit/capture/viewport.test.js +168 -0
  102. package/tests/unit/cli/dispatcher.test.js +25 -0
  103. package/tests/unit/core/navigation-budget.test.js +120 -0
  104. package/tests/unit/core/navigation-manager.test.js +23 -1
  105. package/tests/unit/core/readiness.test.js +21 -0
  106. package/tests/unit/core/trigger-readiness.test.js +39 -0
  107. package/tests/unit/core/webdriver-page.test.js +33 -0
  108. package/tests/unit/elements/reusable-helpers.test.js +83 -0
  109. package/tests/unit/elements/visible-count.test.js +24 -0
  110. package/tests/unit/elements/wait-navigation.test.js +26 -0
  111. package/tests/unit/interactions/navigation-timeout.test.js +39 -0
  112. package/tests/unit/scripts/ci-timeout-budgets.test.js +1 -0
  113. package/tests/unit/traces/bundle.test.js +16 -0
  114. package/tests/unit/traces/links.test.js +2 -2
  115. package/tests/unit/traces/network.test.js +97 -0
@@ -1,7 +1,13 @@
1
1
  import { access } from 'node:fs/promises';
2
+ import { launchFailure } from './launch-diagnostics.js';
3
+ import {
4
+ installSessionPersistence,
5
+ sessionPersistencePath,
6
+ } from './session-persistence.js';
2
7
 
3
8
  import { assertSupportedEngine } from './connector.js';
4
9
  import { emulateMedia } from './media.js';
10
+ import { reserveLoopbackPort } from './debugging-port.js';
5
11
  import {
6
12
  buildPlaywrightLaunchOptions,
7
13
  buildPuppeteerLaunchOptions,
@@ -9,6 +15,7 @@ import {
9
15
  } from './launch-options.js';
10
16
  import {
11
17
  createTemporaryUserDataDir,
18
+ configureUserDataDir,
12
19
  removeUserDataDir,
13
20
  } from './profile-directory.js';
14
21
  import { launchRealBrowser } from './real-browser.js';
@@ -163,6 +170,7 @@ async function launchWithEngine(options, dependencies) {
163
170
  extraArgs,
164
171
  ignoreDefaultArgs,
165
172
  restrictions,
173
+ disableFeatures: options.disableFeatures,
166
174
  });
167
175
  const restrictionEnv = resolveRestrictions(restrictions).env;
168
176
  const childEnv =
@@ -172,6 +180,13 @@ async function launchWithEngine(options, dependencies) {
172
180
  const temporaryProfile = !requestedUserDataDir;
173
181
  const userDataDir =
174
182
  requestedUserDataDir ?? (await createTemporaryUserDataDir());
183
+ await configureUserDataDir(userDataDir, {
184
+ preferences: {
185
+ ...resolveRestrictions(restrictions).preferences,
186
+ ...options.preferences,
187
+ },
188
+ localState: options.localState,
189
+ });
175
190
  const engineModule = await (
176
191
  dependencies.loadEngineModule ?? loadEngine[engine]
177
192
  )();
@@ -302,6 +317,16 @@ export async function launchBrowserWithDependencies(
302
317
  options = {},
303
318
  dependencies = {}
304
319
  ) {
320
+ if (options.keepOpen) {
321
+ const { connectOrLaunch } = await import('./persistent-session.js');
322
+ return connectOrLaunch({
323
+ ...options,
324
+ userDataDir: options.userDataDir ?? (await createTemporaryUserDataDir()),
325
+ remoteDebuggingPort:
326
+ options.remoteDebuggingPort ?? (await reserveLoopbackPort()),
327
+ });
328
+ }
329
+ sessionPersistencePath(options);
305
330
  if (isSafariChannel(options.channel ?? options.browser)) {
306
331
  validateLaunchMode({
307
332
  engine: options.engine ?? 'playwright',
@@ -310,7 +335,13 @@ export async function launchBrowserWithDependencies(
310
335
  });
311
336
  return await launchSafari(options, dependencies);
312
337
  }
313
- return await launchNonSafariBrowser(options, dependencies);
338
+ const result = await launchNonSafariBrowser(options, dependencies);
339
+ try {
340
+ return await installSessionPersistence(result, options);
341
+ } catch (error) {
342
+ await result.close();
343
+ throw error;
344
+ }
314
345
  }
315
346
 
316
347
  async function launchNonSafariBrowser(options, dependencies) {
@@ -343,7 +374,13 @@ async function launchNonSafariBrowser(options, dependencies) {
343
374
  }
344
375
  }
345
376
  // Validate arguments before anything is started or written to disk.
346
- resolveChromeArgs({ args, extraArgs, ignoreDefaultArgs, restrictions });
377
+ resolveChromeArgs({
378
+ args,
379
+ extraArgs,
380
+ ignoreDefaultArgs,
381
+ restrictions,
382
+ disableFeatures: options.disableFeatures,
383
+ });
347
384
  const resolvedStorageState = await loadStorageState(storageState);
348
385
 
349
386
  if (verbose) {
@@ -361,13 +398,21 @@ async function launchNonSafariBrowser(options, dependencies) {
361
398
  automationParity,
362
399
  storageState: resolvedStorageState,
363
400
  };
364
- const launched =
365
- launch === 'engine'
366
- ? await launchWithEngine(
367
- { ...common, ignoreDefaultArgs, colorScheme },
368
- dependencies
369
- )
370
- : await launchReal(common, dependencies);
401
+ let launched;
402
+ try {
403
+ launched =
404
+ launch === 'engine'
405
+ ? await launchWithEngine(
406
+ { ...common, ignoreDefaultArgs, colorScheme },
407
+ dependencies
408
+ )
409
+ : await launchReal(common, dependencies);
410
+ } catch (error) {
411
+ throw launchFailure(error, {
412
+ ...common,
413
+ phase: launch === 'engine' ? 'engine_launch' : 'launch',
414
+ });
415
+ }
371
416
  const { browser, page } = launched;
372
417
  if (verbose) {
373
418
  console.log(`✅ Browser launched with ${engine} engine`);
@@ -3,9 +3,10 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { pathExists } from './fs-utils.js';
5
5
 
6
- import BetterSqlite3 from 'better-sqlite3';
7
-
8
- import { openSqliteDatabase } from '../browser-cookie-database.js';
6
+ import {
7
+ openSqliteDatabase,
8
+ backupSqliteDatabase,
9
+ } from '../browser-cookie-database.js';
9
10
 
10
11
  /**
11
12
  * Read a Chromium SQLite database (History, Top Sites, Login Data, Web Data)
@@ -40,36 +41,13 @@ export async function withDatabaseSnapshot({ sourcePath, read }) {
40
41
  try {
41
42
  // The backup API needs to open the source. It opens read-only, so nothing
42
43
  // is written to the user's real database.
43
- let source;
44
44
  try {
45
- source = new BetterSqlite3(sourcePath, {
46
- readonly: true,
47
- fileMustExist: true,
48
- timeout: 1000,
49
- });
50
- // Detect an exclusive lock before better-sqlite3's initial zero-page
51
- // transfer can mistake SQLITE_BUSY for an empty completed backup.
52
- source.pragma('schema_version');
53
- let remaining = Infinity;
54
- let lastProgress = Date.now();
55
- await source.backup(snapshotPath, {
56
- progress: ({ remainingPages }) => {
57
- if (remainingPages < remaining) {
58
- lastProgress = Date.now();
59
- } else if (Date.now() - lastProgress >= 1000) {
60
- throw new Error('SQLite backup made no progress');
61
- }
62
- remaining = remainingPages;
63
- return 256;
64
- },
65
- });
45
+ await backupSqliteDatabase(sourcePath, snapshotPath);
66
46
  } catch (cause) {
67
47
  throw new Error(
68
48
  `Consistent SQLite snapshot unavailable for ${sourcePath}; close the source browser and retry, or supply a consistent read-only snapshot. ${cause.message}`,
69
49
  { cause }
70
50
  );
71
- } finally {
72
- source?.close();
73
51
  }
74
52
  return await read(snapshotPath);
75
53
  } finally {
@@ -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;