browser-commander 0.17.2 → 0.19.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 +12 -17
- package/CHANGELOG.md +68 -0
- package/README.md +132 -0
- package/eslint.config.js +5 -0
- package/package.json +5 -1
- package/src/bindings.js +13 -1
- package/src/browser/connector.js +15 -3
- package/src/browser/launcher.js +14 -2
- package/src/browser/navigation.js +63 -0
- package/src/core/dialog-manager.js +48 -0
- package/src/core/navigation-manager.js +83 -87
- package/src/core/navigation-readiness.js +133 -0
- package/src/core/readiness.js +478 -0
- package/src/downloads/attach.js +58 -0
- package/src/downloads/destination.js +125 -0
- package/src/downloads/manager.js +476 -0
- package/src/downloads/naming.js +246 -0
- package/src/downloads/sources.js +238 -0
- package/src/downloads/staging.js +146 -0
- package/src/downloads/store.js +256 -0
- package/src/exports.js +86 -0
- package/src/factory.js +55 -0
- package/src/interactions/click-activation.js +317 -0
- package/src/interactions/click-result.js +108 -0
- package/src/interactions/click.js +766 -352
- package/src/tests/index.js +75 -11
- package/src/tests/tracing.js +236 -0
- package/src/traces/bundle.js +338 -0
- package/src/traces/deadline.js +33 -0
- package/src/traces/identity.js +127 -0
- package/src/traces/init-script.js +95 -0
- package/src/traces/links.js +623 -0
- package/src/traces/mutation-stream.js +232 -0
- package/src/traces/observers.js +272 -0
- package/src/traces/page-capture.js +601 -0
- package/src/traces/reader.js +226 -0
- package/src/traces/recorder.js +454 -0
- package/src/traces/redaction.js +231 -0
- package/src/traces/schema.js +229 -0
- package/src/traces/viewer.js +448 -0
- package/tests/e2e/click-readiness.e2e.test.js +240 -0
- package/tests/e2e/downloads.e2e.test.js +235 -0
- package/tests/e2e/traces.e2e.test.js +742 -0
- package/tests/helpers/click-fixtures.js +62 -0
- package/tests/helpers/download-fixtures.js +125 -0
- package/tests/helpers/download-server.js +129 -0
- package/tests/helpers/e2e-browser.js +67 -0
- package/tests/helpers/file-modes.js +31 -0
- package/tests/helpers/fixture-server.js +56 -0
- package/tests/helpers/mocks.js +63 -66
- package/tests/helpers/readiness-server.js +96 -0
- package/tests/helpers/temp-directory.js +44 -0
- package/tests/helpers/trace-fixtures.js +274 -0
- package/tests/helpers/trace-server.js +173 -0
- package/tests/unit/core/dialog-manager.test.js +118 -10
- package/tests/unit/core/navigation-manager.test.js +162 -0
- package/tests/unit/core/readiness.test.js +517 -0
- package/tests/unit/downloads/attach.test.js +220 -0
- package/tests/unit/downloads/destination.test.js +114 -0
- package/tests/unit/downloads/manager.test.js +485 -0
- package/tests/unit/downloads/naming.test.js +174 -0
- package/tests/unit/downloads/staging.test.js +117 -0
- package/tests/unit/downloads/store.test.js +215 -0
- package/tests/unit/interactions/click-activation.test.js +283 -0
- package/tests/unit/interactions/click-result.test.js +90 -0
- package/tests/unit/interactions/click.test.js +409 -200
- package/tests/unit/tests/tracing.test.js +299 -0
- package/tests/unit/traces/bundle.test.js +260 -0
- package/tests/unit/traces/links.test.js +557 -0
- package/tests/unit/traces/reader.test.js +208 -0
- package/tests/unit/traces/recorder.test.js +730 -0
- package/tests/unit/traces/redaction.test.js +170 -0
- package/tests/unit/traces/schema.test.js +92 -0
- package/tests/unit/traces/viewer.test.js +294 -0
|
@@ -28,6 +28,12 @@ export function createDialogManager(options = {}) {
|
|
|
28
28
|
// User-registered dialog handlers
|
|
29
29
|
const handlers = [];
|
|
30
30
|
|
|
31
|
+
// Passive observers. They are told about every dialog and are never
|
|
32
|
+
// responsible for answering one, so watching dialogs - which is what a
|
|
33
|
+
// trace recorder does - cannot leave a page waiting forever for an answer
|
|
34
|
+
// nobody was going to give.
|
|
35
|
+
const observers = [];
|
|
36
|
+
|
|
31
37
|
// Whether we are currently listening to the page's dialog event
|
|
32
38
|
let isListening = false;
|
|
33
39
|
|
|
@@ -45,6 +51,14 @@ export function createDialogManager(options = {}) {
|
|
|
45
51
|
|
|
46
52
|
log.debug(() => `💬 Dialog event: type="${type}", message="${message}"`);
|
|
47
53
|
|
|
54
|
+
for (const observer of observers) {
|
|
55
|
+
try {
|
|
56
|
+
await observer(dialog);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
log.debug(() => `⚠️ Error in dialog observer: ${e.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
if (handlers.length === 0) {
|
|
49
63
|
log.debug(
|
|
50
64
|
() =>
|
|
@@ -121,6 +135,36 @@ export function createDialogManager(options = {}) {
|
|
|
121
135
|
log.debug(() => '🔌 All dialog handlers cleared');
|
|
122
136
|
}
|
|
123
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Watch dialogs without taking responsibility for answering them
|
|
140
|
+
* @param {Function} observer - Async function receiving the dialog object
|
|
141
|
+
* @returns {Function} - Removes this observer again
|
|
142
|
+
*/
|
|
143
|
+
function observeDialogs(observer) {
|
|
144
|
+
if (typeof observer !== 'function') {
|
|
145
|
+
throw new Error('Dialog observer must be a function');
|
|
146
|
+
}
|
|
147
|
+
observers.push(observer);
|
|
148
|
+
log.debug(
|
|
149
|
+
() => `👁 Dialog observer registered (total: ${observers.length})`
|
|
150
|
+
);
|
|
151
|
+
return () => unobserveDialogs(observer);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Remove a dialog observer
|
|
156
|
+
* @param {Function} observer - The observer to remove
|
|
157
|
+
*/
|
|
158
|
+
function unobserveDialogs(observer) {
|
|
159
|
+
const index = observers.indexOf(observer);
|
|
160
|
+
if (index !== -1) {
|
|
161
|
+
observers.splice(index, 1);
|
|
162
|
+
log.debug(
|
|
163
|
+
() => `👁 Dialog observer removed (remaining: ${observers.length})`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
124
168
|
/**
|
|
125
169
|
* Start listening for dialog events on the page
|
|
126
170
|
*/
|
|
@@ -151,6 +195,10 @@ export function createDialogManager(options = {}) {
|
|
|
151
195
|
offDialog,
|
|
152
196
|
clearDialogHandlers,
|
|
153
197
|
|
|
198
|
+
// Passive observation (issue #87)
|
|
199
|
+
observeDialogs,
|
|
200
|
+
unobserveDialogs,
|
|
201
|
+
|
|
154
202
|
// Lifecycle
|
|
155
203
|
startListening,
|
|
156
204
|
stopListening,
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import { isNavigationError } from './navigation-safety.js';
|
|
12
12
|
import { TIMING } from './constants.js';
|
|
13
|
+
import { READINESS_STATUS } from './readiness.js';
|
|
14
|
+
import { createReadinessWaiter } from './navigation-readiness.js';
|
|
13
15
|
|
|
14
16
|
async function setManagedContent(options = {}) {
|
|
15
17
|
const {
|
|
@@ -22,7 +24,7 @@ async function setManagedContent(options = {}) {
|
|
|
22
24
|
triggerNavigationStart,
|
|
23
25
|
updateCurrentUrl,
|
|
24
26
|
waitForPageReady,
|
|
25
|
-
|
|
27
|
+
abandonNavigation,
|
|
26
28
|
} = options;
|
|
27
29
|
|
|
28
30
|
if (typeof html !== 'string') {
|
|
@@ -42,8 +44,9 @@ async function setManagedContent(options = {}) {
|
|
|
42
44
|
await waitForPageReady({ timeout, reason: 'after setContent' });
|
|
43
45
|
return true;
|
|
44
46
|
} catch (error) {
|
|
45
|
-
// Do not leave the manager in its loading state when setContent fails
|
|
46
|
-
|
|
47
|
+
// Do not leave the manager in its loading state when setContent fails, but
|
|
48
|
+
// do not claim the page became ready either - it did not.
|
|
49
|
+
abandonNavigation('setContent failed');
|
|
47
50
|
if (isNavigationError(error)) {
|
|
48
51
|
log.debug(() => '⚠️ Content loading was interrupted, recovering...');
|
|
49
52
|
return false;
|
|
@@ -211,21 +214,44 @@ export function createNavigationManager(options = {}) {
|
|
|
211
214
|
}
|
|
212
215
|
}
|
|
213
216
|
|
|
214
|
-
// Track if
|
|
217
|
+
// Track if a readiness wait is currently running to prevent concurrent calls
|
|
215
218
|
let pageReadyPromise = null;
|
|
216
219
|
|
|
220
|
+
const { waitForReady } = createReadinessWaiter({
|
|
221
|
+
page,
|
|
222
|
+
engine,
|
|
223
|
+
log,
|
|
224
|
+
networkTracker,
|
|
225
|
+
config,
|
|
226
|
+
getState: () => ({ url: currentUrl, sessionId }),
|
|
227
|
+
onUrlSample: (url) => {
|
|
228
|
+
if (url !== currentUrl) {
|
|
229
|
+
currentUrl = url;
|
|
230
|
+
log.debug(() => `🔄 Redirect detected: ${url}`);
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
onReady: () => {
|
|
234
|
+
finishNavigationTracking({ ready: true });
|
|
235
|
+
emitPageReady();
|
|
236
|
+
},
|
|
237
|
+
onNotReady: () => finishNavigationTracking({ ready: false }),
|
|
238
|
+
});
|
|
239
|
+
|
|
217
240
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* @
|
|
241
|
+
* Boolean-returning readiness wait kept for backward compatibility.
|
|
242
|
+
*
|
|
243
|
+
* Unlike the previous implementation this returns `false` when the page did
|
|
244
|
+
* not actually become ready, and it never emits the page-ready event in that
|
|
245
|
+
* case. Prefer {@link waitForReady} for the structured result.
|
|
246
|
+
*
|
|
247
|
+
* @param {Object} [opts] - Same options as {@link waitForReady}
|
|
248
|
+
* @returns {Promise<boolean>} Whether the page genuinely became ready
|
|
223
249
|
*/
|
|
224
250
|
async function waitForPageReady(opts = {}) {
|
|
225
|
-
const {
|
|
251
|
+
const { reason = 'page ready' } = opts;
|
|
226
252
|
|
|
227
|
-
//
|
|
228
|
-
//
|
|
253
|
+
// Concurrent callers join the in-flight wait instead of racing a second
|
|
254
|
+
// deadline against the first.
|
|
229
255
|
if (pageReadyPromise) {
|
|
230
256
|
log.debug(
|
|
231
257
|
() => `⏳ Waiting for existing page ready operation (${reason})...`
|
|
@@ -233,74 +259,38 @@ export function createNavigationManager(options = {}) {
|
|
|
233
259
|
return pageReadyPromise;
|
|
234
260
|
}
|
|
235
261
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// Create the promise and store it
|
|
239
|
-
pageReadyPromise = (async () => {
|
|
240
|
-
const startTime = Date.now();
|
|
241
|
-
let lastUrlChangeTime = Date.now();
|
|
242
|
-
|
|
243
|
-
// Wait for URL to stabilize (no more redirects)
|
|
244
|
-
while (
|
|
245
|
-
Date.now() - lastUrlChangeTime <
|
|
246
|
-
config.redirectStabilizationTime
|
|
247
|
-
) {
|
|
248
|
-
if (Date.now() - startTime > timeout) {
|
|
249
|
-
log.debug(
|
|
250
|
-
() => `⚠️ Page ready timeout after ${timeout}ms (${reason})`
|
|
251
|
-
);
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
256
|
-
|
|
257
|
-
// Check if URL changed
|
|
258
|
-
const nowUrl = page.url();
|
|
259
|
-
if (nowUrl !== currentUrl) {
|
|
260
|
-
currentUrl = nowUrl;
|
|
261
|
-
lastUrlChangeTime = Date.now();
|
|
262
|
-
log.debug(() => `🔄 Redirect detected: ${nowUrl}`);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Wait for network idle - use remaining time but ensure at least 30s for idle check
|
|
267
|
-
// The 30s idle time is enforced by the network tracker's idleTimeout config
|
|
268
|
-
if (networkTracker) {
|
|
269
|
-
const elapsed = Date.now() - startTime;
|
|
270
|
-
// Give at least 60 seconds for network idle, or remaining time if more
|
|
271
|
-
const remainingTimeout = Math.max(60000, timeout - elapsed);
|
|
272
|
-
|
|
273
|
-
const networkIdle = await networkTracker.waitForNetworkIdle({
|
|
274
|
-
timeout: remainingTimeout,
|
|
275
|
-
// idleTime defaults to TIMING.NAVIGATION_TIMEOUT from tracker config
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
if (!networkIdle) {
|
|
279
|
-
log.debug(() => `⚠️ Network did not become idle (${reason})`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Complete navigation
|
|
284
|
-
completeNavigation();
|
|
285
|
-
|
|
286
|
-
const elapsed = Date.now() - startTime;
|
|
287
|
-
log.debug(() => `✅ Page ready after ${elapsed}ms (${reason})`);
|
|
288
|
-
|
|
289
|
-
return true;
|
|
290
|
-
})();
|
|
262
|
+
pageReadyPromise = waitForReady(opts).then((result) => result.ready);
|
|
291
263
|
|
|
292
264
|
try {
|
|
293
265
|
return await pageReadyPromise;
|
|
294
266
|
} finally {
|
|
295
|
-
// Clear the promise so next call can start fresh
|
|
296
267
|
pageReadyPromise = null;
|
|
297
268
|
}
|
|
298
269
|
}
|
|
299
270
|
|
|
300
271
|
/**
|
|
301
|
-
*
|
|
272
|
+
* Leave the navigating state without claiming the page became ready.
|
|
273
|
+
*
|
|
274
|
+
* @param {string} reason - Why the navigation was abandoned
|
|
275
|
+
*/
|
|
276
|
+
function abandonNavigation(reason) {
|
|
277
|
+
log.debug(() => `⚠️ Navigation abandoned: ${reason}`);
|
|
278
|
+
finishNavigationTracking({ ready: false });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Close out navigation bookkeeping.
|
|
283
|
+
*
|
|
284
|
+
* This only reports that the navigation stopped being in flight. Whether the
|
|
285
|
+
* page is usable is a separate question answered by {@link emitPageReady},
|
|
286
|
+
* which is why the two are no longer one function.
|
|
287
|
+
*
|
|
288
|
+
* @param {Object} [options] - Configuration options
|
|
289
|
+
* @param {boolean} [options.ready=false] - Whether the page reached a ready state
|
|
302
290
|
*/
|
|
303
|
-
function
|
|
291
|
+
function finishNavigationTracking(options = {}) {
|
|
292
|
+
const { ready = false } = options;
|
|
293
|
+
|
|
304
294
|
if (!isNavigating) {
|
|
305
295
|
return;
|
|
306
296
|
}
|
|
@@ -310,13 +300,14 @@ export function createNavigationManager(options = {}) {
|
|
|
310
300
|
navigationStartTime = null;
|
|
311
301
|
|
|
312
302
|
log.debug(
|
|
313
|
-
() =>
|
|
303
|
+
() =>
|
|
304
|
+
`${ready ? '✅' : '⚠️ '} Navigation finished (session ${sessionId}, ` +
|
|
305
|
+
`${duration}ms, ready=${ready})`
|
|
314
306
|
);
|
|
315
307
|
|
|
316
|
-
// Notify navigation complete listeners
|
|
317
308
|
listeners.onNavigationComplete.forEach((fn) => {
|
|
318
309
|
try {
|
|
319
|
-
fn({ url: currentUrl, sessionId, duration });
|
|
310
|
+
fn({ url: currentUrl, sessionId, duration, ready });
|
|
320
311
|
} catch (e) {
|
|
321
312
|
log.debug(
|
|
322
313
|
() => `⚠️ Error in onNavigationComplete listener: ${e.message}`
|
|
@@ -324,7 +315,17 @@ export function createNavigationManager(options = {}) {
|
|
|
324
315
|
}
|
|
325
316
|
});
|
|
326
317
|
|
|
327
|
-
|
|
318
|
+
if (navigationResolve) {
|
|
319
|
+
navigationResolve(ready);
|
|
320
|
+
navigationResolve = null;
|
|
321
|
+
navigationPromise = null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Announce that the page reached a ready state.
|
|
327
|
+
*/
|
|
328
|
+
function emitPageReady() {
|
|
328
329
|
listeners.onPageReady.forEach((fn) => {
|
|
329
330
|
try {
|
|
330
331
|
fn({ url: currentUrl, sessionId });
|
|
@@ -332,13 +333,6 @@ export function createNavigationManager(options = {}) {
|
|
|
332
333
|
log.debug(() => `⚠️ Error in onPageReady listener: ${e.message}`);
|
|
333
334
|
}
|
|
334
335
|
});
|
|
335
|
-
|
|
336
|
-
// Resolve navigation promise if waiting
|
|
337
|
-
if (navigationResolve) {
|
|
338
|
-
navigationResolve(true);
|
|
339
|
-
navigationResolve = null;
|
|
340
|
-
navigationPromise = null;
|
|
341
|
-
}
|
|
342
336
|
}
|
|
343
337
|
|
|
344
338
|
/**
|
|
@@ -369,13 +363,11 @@ export function createNavigationManager(options = {}) {
|
|
|
369
363
|
currentUrl = page.url();
|
|
370
364
|
|
|
371
365
|
// Wait for page to be fully ready
|
|
372
|
-
await waitForPageReady({ timeout, reason: 'after goto' });
|
|
373
|
-
|
|
374
|
-
return true;
|
|
366
|
+
return await waitForPageReady({ timeout, reason: 'after goto' });
|
|
375
367
|
} catch (error) {
|
|
376
368
|
if (isNavigationError(error)) {
|
|
377
369
|
log.debug(() => '⚠️ Navigation was interrupted, recovering...');
|
|
378
|
-
|
|
370
|
+
abandonNavigation('navigation interrupted');
|
|
379
371
|
return false;
|
|
380
372
|
}
|
|
381
373
|
throw error;
|
|
@@ -391,7 +383,7 @@ export function createNavigationManager(options = {}) {
|
|
|
391
383
|
triggerNavigationStart,
|
|
392
384
|
updateCurrentUrl: () => (currentUrl = page.url()),
|
|
393
385
|
waitForPageReady,
|
|
394
|
-
|
|
386
|
+
abandonNavigation,
|
|
395
387
|
});
|
|
396
388
|
|
|
397
389
|
/**
|
|
@@ -416,7 +408,7 @@ export function createNavigationManager(options = {}) {
|
|
|
416
408
|
setTimeout(() => {
|
|
417
409
|
if (isNavigating) {
|
|
418
410
|
log.debug(() => '⚠️ waitForNavigation timeout');
|
|
419
|
-
|
|
411
|
+
abandonNavigation('waitForNavigation timeout');
|
|
420
412
|
resolve(false);
|
|
421
413
|
}
|
|
422
414
|
}, timeout);
|
|
@@ -511,6 +503,7 @@ export function createNavigationManager(options = {}) {
|
|
|
511
503
|
setContent,
|
|
512
504
|
waitForNavigation,
|
|
513
505
|
waitForPageReady,
|
|
506
|
+
waitForReady,
|
|
514
507
|
|
|
515
508
|
// State
|
|
516
509
|
isNavigating: () => isNavigating,
|
|
@@ -532,5 +525,8 @@ export function createNavigationManager(options = {}) {
|
|
|
532
525
|
startListening,
|
|
533
526
|
stopListening,
|
|
534
527
|
configure,
|
|
528
|
+
|
|
529
|
+
// Readiness status vocabulary, re-exported for callers matching on it
|
|
530
|
+
READINESS_STATUS,
|
|
535
531
|
};
|
|
536
532
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readiness wiring for the navigation manager.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from `navigation-manager.js` so the two halves of "the page
|
|
5
|
+
* stopped navigating" and "the page is actually usable" stay visibly distinct:
|
|
6
|
+
* this module decides only the second question and reports the evidence behind
|
|
7
|
+
* its answer.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createDeadline,
|
|
12
|
+
networkIdleFor,
|
|
13
|
+
runReadinessChecks,
|
|
14
|
+
urlStableFor,
|
|
15
|
+
} from './readiness.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build the checks that define "ready" when a caller does not supply their own.
|
|
19
|
+
*
|
|
20
|
+
* @param {Object} config - Navigation manager configuration
|
|
21
|
+
* @param {number} config.redirectStabilizationTime - Required URL quiet period
|
|
22
|
+
* @returns {Array} Ordered readiness checks
|
|
23
|
+
*/
|
|
24
|
+
export function defaultReadinessChecks(config) {
|
|
25
|
+
return [
|
|
26
|
+
urlStableFor({
|
|
27
|
+
stableForMs: config.redirectStabilizationTime,
|
|
28
|
+
intervalMs: 200,
|
|
29
|
+
}),
|
|
30
|
+
networkIdleFor(),
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create the readiness waiter used by the navigation manager.
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} options - Configuration options
|
|
38
|
+
* @param {Object} options.page - Browser page object
|
|
39
|
+
* @param {string} options.engine - Engine name
|
|
40
|
+
* @param {Function} options.log - Logger instance
|
|
41
|
+
* @param {Object} [options.networkTracker] - NetworkTracker instance
|
|
42
|
+
* @param {Object} options.config - Navigation manager configuration
|
|
43
|
+
* @param {Function} options.getState - Returns `{currentUrl, sessionId}`
|
|
44
|
+
* @param {Function} options.onUrlSample - Called with every sampled URL
|
|
45
|
+
* @param {Function} options.onReady - Called when every check passed
|
|
46
|
+
* @param {Function} options.onNotReady - Called when the wait did not succeed
|
|
47
|
+
* @returns {{waitForReady: Function, getAdapter: Function}} Readiness API
|
|
48
|
+
*/
|
|
49
|
+
export function createReadinessWaiter(options = {}) {
|
|
50
|
+
const {
|
|
51
|
+
page,
|
|
52
|
+
engine,
|
|
53
|
+
log,
|
|
54
|
+
networkTracker,
|
|
55
|
+
config,
|
|
56
|
+
getState,
|
|
57
|
+
onUrlSample,
|
|
58
|
+
onReady,
|
|
59
|
+
onNotReady,
|
|
60
|
+
} = options;
|
|
61
|
+
|
|
62
|
+
let cachedAdapter;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Lazily build an engine adapter for checks that need to evaluate in-page.
|
|
66
|
+
* Kept lazy so the default checks never touch a page that cannot evaluate.
|
|
67
|
+
*
|
|
68
|
+
* @returns {Promise<Object>} Engine adapter
|
|
69
|
+
*/
|
|
70
|
+
async function getAdapter() {
|
|
71
|
+
if (cachedAdapter === undefined) {
|
|
72
|
+
const { createEngineAdapter } = await import('./engine-adapter.js');
|
|
73
|
+
cachedAdapter = createEngineAdapter(page, engine);
|
|
74
|
+
}
|
|
75
|
+
return cachedAdapter;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Wait for the page to be ready and report exactly what was observed.
|
|
80
|
+
*
|
|
81
|
+
* Every check shares one monotonic deadline, so the total wait can never
|
|
82
|
+
* exceed `timeout` no matter how many checks run or how slow each one is.
|
|
83
|
+
* The page-ready event fires only when every check actually passed.
|
|
84
|
+
*
|
|
85
|
+
* @param {Object} [opts] - Configuration options
|
|
86
|
+
* @param {number} [opts.timeout] - Total budget in milliseconds
|
|
87
|
+
* @param {string} [opts.reason] - Reason for waiting (for logging)
|
|
88
|
+
* @param {Array} [opts.checks] - Composable readiness checks to run
|
|
89
|
+
* @returns {Promise<Object>} Structured readiness result
|
|
90
|
+
*/
|
|
91
|
+
async function waitForReady(opts = {}) {
|
|
92
|
+
const {
|
|
93
|
+
timeout = config.networkIdleTimeout,
|
|
94
|
+
reason = 'page ready',
|
|
95
|
+
checks,
|
|
96
|
+
} = opts;
|
|
97
|
+
|
|
98
|
+
log.debug(() => `⏳ Waiting for page ready (${reason})...`);
|
|
99
|
+
|
|
100
|
+
const deadline = createDeadline({ timeout });
|
|
101
|
+
const result = await runReadinessChecks({
|
|
102
|
+
checks: checks ?? defaultReadinessChecks(config),
|
|
103
|
+
deadline,
|
|
104
|
+
context: {
|
|
105
|
+
page,
|
|
106
|
+
engine,
|
|
107
|
+
log,
|
|
108
|
+
networkTracker,
|
|
109
|
+
getAdapter,
|
|
110
|
+
onUrlSample,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const outcome = { ...result, reason, ...getState() };
|
|
115
|
+
|
|
116
|
+
if (outcome.ready) {
|
|
117
|
+
onReady();
|
|
118
|
+
log.debug(() => `✅ Page ready after ${outcome.elapsedMs}ms (${reason})`);
|
|
119
|
+
} else {
|
|
120
|
+
onNotReady();
|
|
121
|
+
log.debug(
|
|
122
|
+
() =>
|
|
123
|
+
`⚠️ Page not ready (${reason}): ${outcome.status} after ` +
|
|
124
|
+
`${outcome.elapsedMs}ms; failed=[${outcome.checks.failed.join(', ')}] ` +
|
|
125
|
+
`pending=[${outcome.checks.pending.join(', ')}]`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return outcome;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { waitForReady, getAdapter };
|
|
133
|
+
}
|