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.
- package/.jscpd-baseline.json +24 -23
- package/CHANGELOG.md +22 -0
- package/README.md +6 -0
- package/eslint.config.js +11 -0
- package/package.json +9 -4
- 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/connector.js +117 -13
- 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/launch-options.js +2 -1
- package/src/browser/launch-restrictions.json +33 -2
- package/src/browser/launcher.js +54 -9
- package/src/browser/migration/sqlite-snapshot.js +5 -27
- package/src/browser/navigation.js +173 -98
- package/src/browser/persistent-session.js +360 -0
- package/src/browser/profile-directory.js +20 -0
- package/src/browser/real-browser.js +142 -70
- package/src/browser/restrictions.js +13 -4
- package/src/browser/session-cookies.js +78 -0
- package/src/browser/session-persistence.js +72 -0
- package/src/browser/session-watchdog.js +71 -0
- package/src/browser/session-worker.js +70 -0
- package/src/browser/storage-state.js +32 -6
- package/src/browser/webdriver.js +4 -0
- package/src/capture/encoding.js +327 -0
- package/src/capture/errors.js +12 -0
- package/src/capture/files.js +9 -0
- package/src/capture/index.js +5 -0
- package/src/capture/recording.js +136 -0
- package/src/capture/screenshot.js +222 -0
- package/src/capture/video.js +186 -0
- package/src/capture/viewport.js +68 -0
- package/src/capture/worker.js +22 -0
- package/src/cli/args.js +66 -1
- package/src/cli/commands.js +113 -2
- package/src/cli/high-level-methods.js +40 -3
- package/src/cli/sessions.js +11 -3
- package/src/core/commander-features.js +105 -0
- package/src/core/constants.js +1 -0
- package/src/core/dialog-manager.js +7 -0
- package/src/core/managed-navigation.js +101 -0
- package/src/core/navigation-manager.js +66 -80
- package/src/core/navigation-operation.js +25 -0
- package/src/core/navigation-readiness.js +68 -16
- package/src/core/navigation-safety.js +51 -0
- package/src/core/network-tracker.js +39 -92
- package/src/core/page-session.js +3 -2
- package/src/core/page-trigger-manager.js +130 -42
- package/src/core/readiness.js +76 -14
- package/src/core/subscriptions.js +20 -0
- package/src/core/webdriver-page.js +38 -0
- package/src/elements/locators.js +61 -37
- package/src/elements/reusable.js +92 -0
- package/src/elements/selectors.js +37 -81
- package/src/elements/visibility.js +38 -21
- package/src/exports.js +14 -0
- package/src/factory.js +17 -16
- package/src/high-level/universal-logic.js +57 -21
- package/src/index.js +2 -0
- package/src/interactions/click.js +47 -0
- package/src/interactions/overlays.js +28 -0
- package/src/interactions/scroll.js +8 -2
- package/src/traces/bundle.js +14 -5
- package/src/traces/checkpoints.js +99 -0
- package/src/traces/links.js +117 -1
- package/src/traces/mutation-stream.js +1 -0
- package/src/traces/network.js +257 -0
- package/src/traces/page-capture.js +143 -52
- package/src/traces/reader.js +8 -10
- package/src/traces/recorder.js +221 -245
- package/src/traces/render.js +168 -0
- package/src/traces/rolling.js +180 -0
- package/src/traces/start-cleanup.js +15 -0
- package/src/traces/stop.js +159 -0
- package/src/traces/storage.js +43 -0
- package/src/utilities/subprocess.js +47 -4
- package/tests/e2e/cli.e2e.test.js +20 -1
- package/tests/e2e/click-readiness.e2e.test.js +1 -1
- 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-building-blocks.test.js +5 -1
- package/tests/unit/browser/launch-diagnostics.test.js +52 -0
- package/tests/unit/browser/launcher.test.js +5 -1
- package/tests/unit/browser/persistent-session.test.js +59 -0
- package/tests/unit/browser/quiet-ui.test.js +16 -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/browser/tab-selection.test.js +38 -0
- package/tests/unit/capture/capture.test.js +52 -0
- package/tests/unit/capture/cleanup.test.js +24 -0
- package/tests/unit/capture/viewport.test.js +168 -0
- package/tests/unit/cli/dispatcher.test.js +25 -0
- package/tests/unit/core/navigation-budget.test.js +120 -0
- package/tests/unit/core/navigation-manager.test.js +23 -1
- package/tests/unit/core/readiness.test.js +21 -0
- package/tests/unit/core/trigger-readiness.test.js +39 -0
- package/tests/unit/core/webdriver-page.test.js +33 -0
- package/tests/unit/elements/reusable-helpers.test.js +83 -0
- package/tests/unit/elements/visible-count.test.js +24 -0
- package/tests/unit/elements/wait-navigation.test.js +26 -0
- package/tests/unit/interactions/navigation-timeout.test.js +39 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +1 -0
- package/tests/unit/traces/bundle.test.js +16 -0
- package/tests/unit/traces/links.test.js +2 -2
- package/tests/unit/traces/network.test.js +97 -0
package/src/browser/launcher.js
CHANGED
|
@@ -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
|
-
|
|
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({
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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;
|