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
|
@@ -266,10 +266,98 @@ describe('dialog-manager', () => {
|
|
|
266
266
|
});
|
|
267
267
|
});
|
|
268
268
|
|
|
269
|
+
describe('observeDialogs', () => {
|
|
270
|
+
/**
|
|
271
|
+
* A manager already listening to the page, as every observer test needs.
|
|
272
|
+
*
|
|
273
|
+
* @returns {Object} The dialog manager
|
|
274
|
+
*/
|
|
275
|
+
const listening = () => {
|
|
276
|
+
const manager = createDialogManager({ page, engine: 'playwright', log });
|
|
277
|
+
manager.startListening();
|
|
278
|
+
return manager;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
it('should still auto-dismiss when only observers are watching', async () => {
|
|
282
|
+
const manager = listening();
|
|
283
|
+
|
|
284
|
+
const seen = [];
|
|
285
|
+
manager.observeDialogs((dialog) => seen.push(dialog.message()));
|
|
286
|
+
const dialog = createMockDialog({ message: 'watched' });
|
|
287
|
+
await page.emit('dialog', dialog);
|
|
288
|
+
|
|
289
|
+
assert.deepStrictEqual(seen, ['watched']);
|
|
290
|
+
// Watching a dialog must not make the watcher responsible for it, or a
|
|
291
|
+
// traced run leaves the page waiting for an answer an untraced run got.
|
|
292
|
+
assert.strictEqual(dialog._wasDismissed(), true);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should tell observers about a dialog a handler answers', async () => {
|
|
296
|
+
const manager = listening();
|
|
297
|
+
|
|
298
|
+
const seen = [];
|
|
299
|
+
manager.observeDialogs((dialog) => seen.push(dialog.type()));
|
|
300
|
+
manager.onDialog(async (dialog) => dialog.accept('ok'));
|
|
301
|
+
const dialog = createMockDialog({ type: 'prompt' });
|
|
302
|
+
await page.emit('dialog', dialog);
|
|
303
|
+
|
|
304
|
+
assert.deepStrictEqual(seen, ['prompt']);
|
|
305
|
+
assert.strictEqual(dialog._acceptText(), 'ok');
|
|
306
|
+
assert.strictEqual(dialog._wasDismissed(), false);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should keep going when an observer throws', async () => {
|
|
310
|
+
const manager = listening();
|
|
311
|
+
|
|
312
|
+
let secondObserverCalled = false;
|
|
313
|
+
manager.observeDialogs(() => {
|
|
314
|
+
throw new Error('observer failed');
|
|
315
|
+
});
|
|
316
|
+
manager.observeDialogs(() => {
|
|
317
|
+
secondObserverCalled = true;
|
|
318
|
+
});
|
|
319
|
+
const dialog = createMockDialog();
|
|
320
|
+
await page.emit('dialog', dialog);
|
|
321
|
+
|
|
322
|
+
assert.strictEqual(secondObserverCalled, true);
|
|
323
|
+
assert.strictEqual(dialog._wasDismissed(), true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should stop telling an observer that was removed', async () => {
|
|
327
|
+
const manager = listening();
|
|
328
|
+
|
|
329
|
+
let calls = 0;
|
|
330
|
+
const stop = manager.observeDialogs(() => {
|
|
331
|
+
calls += 1;
|
|
332
|
+
});
|
|
333
|
+
await page.emit('dialog', createMockDialog());
|
|
334
|
+
stop();
|
|
335
|
+
await page.emit('dialog', createMockDialog());
|
|
336
|
+
|
|
337
|
+
assert.strictEqual(calls, 1);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should refuse an observer that is not a function', () => {
|
|
341
|
+
const manager = createDialogManager({ page, engine: 'playwright', log });
|
|
342
|
+
|
|
343
|
+
assert.throws(() => manager.observeDialogs('nope'), /must be a function/);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
269
347
|
describe('integration with makeBrowserCommander', () => {
|
|
270
|
-
|
|
348
|
+
/**
|
|
349
|
+
* A commander driving the suite's page.
|
|
350
|
+
*
|
|
351
|
+
* @param {Object} [options] - Extra options for `makeBrowserCommander`
|
|
352
|
+
* @returns {Promise<Object>} The commander
|
|
353
|
+
*/
|
|
354
|
+
const commanderFor = async (options = {}) => {
|
|
271
355
|
const { makeBrowserCommander } = await import('../../../src/factory.js');
|
|
272
|
-
|
|
356
|
+
return makeBrowserCommander({ page, verbose: false, ...options });
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
it('should expose onDialog on commander', async () => {
|
|
360
|
+
const commander = await commanderFor();
|
|
273
361
|
|
|
274
362
|
assert.ok(typeof commander.onDialog === 'function');
|
|
275
363
|
assert.ok(typeof commander.offDialog === 'function');
|
|
@@ -278,8 +366,7 @@ describe('dialog-manager', () => {
|
|
|
278
366
|
});
|
|
279
367
|
|
|
280
368
|
it('should handle dialog via commander.onDialog', async () => {
|
|
281
|
-
const
|
|
282
|
-
const commander = makeBrowserCommander({ page, verbose: false });
|
|
369
|
+
const commander = await commanderFor();
|
|
283
370
|
|
|
284
371
|
let handlerCalled = false;
|
|
285
372
|
commander.onDialog(async (dialog) => {
|
|
@@ -296,13 +383,34 @@ describe('dialog-manager', () => {
|
|
|
296
383
|
await commander.destroy();
|
|
297
384
|
});
|
|
298
385
|
|
|
386
|
+
it('should expose observeDialogs on commander', async () => {
|
|
387
|
+
const commander = await commanderFor();
|
|
388
|
+
|
|
389
|
+
const seen = [];
|
|
390
|
+
commander.observeDialogs((dialog) => seen.push(dialog.message()));
|
|
391
|
+
const dialog = createMockDialog({ message: 'from the commander' });
|
|
392
|
+
await page.emit('dialog', dialog);
|
|
393
|
+
|
|
394
|
+
assert.deepStrictEqual(seen, ['from the commander']);
|
|
395
|
+
assert.strictEqual(dialog._wasDismissed(), true);
|
|
396
|
+
|
|
397
|
+
await commander.destroy();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should throw observeDialogs when enableDialogManager is false', async () => {
|
|
401
|
+
const commander = await commanderFor({ enableDialogManager: false });
|
|
402
|
+
|
|
403
|
+
assert.throws(
|
|
404
|
+
() => commander.observeDialogs(() => {}),
|
|
405
|
+
/enableDialogManager/
|
|
406
|
+
);
|
|
407
|
+
// Removing an observer from a manager that never existed is a no-op,
|
|
408
|
+
// so cleanup code does not have to know whether tracing was on.
|
|
409
|
+
assert.doesNotThrow(() => commander.unobserveDialogs(() => {}));
|
|
410
|
+
});
|
|
411
|
+
|
|
299
412
|
it('should throw onDialog when enableDialogManager is false', async () => {
|
|
300
|
-
const
|
|
301
|
-
const commander = makeBrowserCommander({
|
|
302
|
-
page,
|
|
303
|
-
verbose: false,
|
|
304
|
-
enableDialogManager: false,
|
|
305
|
-
});
|
|
413
|
+
const commander = await commanderFor({ enableDialogManager: false });
|
|
306
414
|
|
|
307
415
|
assert.throws(() => commander.onDialog(() => {}), /enableDialogManager/);
|
|
308
416
|
});
|
|
@@ -4,7 +4,33 @@ import { createNavigationManager } from '../../../src/core/navigation-manager.js
|
|
|
4
4
|
import {
|
|
5
5
|
createMockPlaywrightPage,
|
|
6
6
|
createMockLogger,
|
|
7
|
+
createMockNetworkTracker,
|
|
7
8
|
} from '../../helpers/mocks.js';
|
|
9
|
+
import { READINESS_STATUS } from '../../../src/core/readiness.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build a navigation manager wired to a network tracker whose idle wait is
|
|
13
|
+
* fully controlled by the test.
|
|
14
|
+
*/
|
|
15
|
+
function createManagerWithNetwork(options = {}) {
|
|
16
|
+
const { idle = true, onIdleWait = () => {} } = options;
|
|
17
|
+
const page = createMockPlaywrightPage();
|
|
18
|
+
const log = createMockLogger();
|
|
19
|
+
const networkTracker = createMockNetworkTracker();
|
|
20
|
+
networkTracker.waitForNetworkIdle = async (opts = {}) => {
|
|
21
|
+
onIdleWait(opts);
|
|
22
|
+
return typeof idle === 'function' ? await idle(opts) : idle;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const manager = createNavigationManager({
|
|
26
|
+
page,
|
|
27
|
+
engine: 'playwright',
|
|
28
|
+
log,
|
|
29
|
+
networkTracker,
|
|
30
|
+
});
|
|
31
|
+
manager.configure({ redirectStabilizationTime: 0 });
|
|
32
|
+
return { manager, page, log, networkTracker };
|
|
33
|
+
}
|
|
8
34
|
|
|
9
35
|
describe('navigation manager', () => {
|
|
10
36
|
describe('setContent', () => {
|
|
@@ -59,4 +85,140 @@ describe('navigation manager', () => {
|
|
|
59
85
|
await assert.doesNotReject(() => manager.setContent({ html: '' }));
|
|
60
86
|
});
|
|
61
87
|
});
|
|
88
|
+
|
|
89
|
+
describe('waitForReady', () => {
|
|
90
|
+
it('should never extend the caller deadline beyond the timeout', async () => {
|
|
91
|
+
// Regression test for issue #89: the old implementation waited
|
|
92
|
+
// `Math.max(60000, timeout - elapsed)`, so a 500ms budget could block for
|
|
93
|
+
// a full minute.
|
|
94
|
+
const idleTimeouts = [];
|
|
95
|
+
const { manager } = createManagerWithNetwork({
|
|
96
|
+
idle: true,
|
|
97
|
+
onIdleWait: (opts) => idleTimeouts.push(opts.timeout),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await manager.waitForReady({ timeout: 1000, reason: 'deadline test' });
|
|
101
|
+
|
|
102
|
+
assert.strictEqual(idleTimeouts.length, 1);
|
|
103
|
+
assert.ok(
|
|
104
|
+
idleTimeouts[0] <= 1000,
|
|
105
|
+
`network idle timeout ${idleTimeouts[0]} exceeded the 1000ms budget`
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should report timed_out instead of claiming the page is ready', async () => {
|
|
110
|
+
// Regression test for issue #89: `waitForPageReady` logged "Page ready"
|
|
111
|
+
// and returned true even after network idle had failed.
|
|
112
|
+
const { manager } = createManagerWithNetwork({
|
|
113
|
+
// Burn the whole remaining budget, the way a real idle wait does.
|
|
114
|
+
idle: async (opts) => {
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, opts.timeout));
|
|
116
|
+
return false;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const result = await manager.waitForReady({
|
|
121
|
+
timeout: 600,
|
|
122
|
+
reason: 'failure test',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
assert.strictEqual(result.ready, false);
|
|
126
|
+
assert.strictEqual(result.status, READINESS_STATUS.TIMED_OUT);
|
|
127
|
+
assert.ok(result.checks.failed.includes('networkIdleFor'));
|
|
128
|
+
assert.ok(
|
|
129
|
+
result.elapsedMs < 3000,
|
|
130
|
+
`wait took ${result.elapsedMs}ms for a 600ms budget`
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should report failed when a check fails before the deadline', async () => {
|
|
135
|
+
const { manager } = createManagerWithNetwork({ idle: false });
|
|
136
|
+
|
|
137
|
+
const result = await manager.waitForReady({ timeout: 5000 });
|
|
138
|
+
|
|
139
|
+
assert.strictEqual(result.ready, false);
|
|
140
|
+
assert.strictEqual(result.status, READINESS_STATUS.FAILED);
|
|
141
|
+
assert.ok(result.checks.failed.includes('networkIdleFor'));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should not emit onPageReady after a failed wait', async () => {
|
|
145
|
+
// Regression test for issue #89: cleanup and the ready event were both
|
|
146
|
+
// owned by completeNavigation(), so failing waits still announced ready.
|
|
147
|
+
const { manager } = createManagerWithNetwork({ idle: false });
|
|
148
|
+
const events = [];
|
|
149
|
+
manager.on('onPageReady', () => events.push('pageReady'));
|
|
150
|
+
manager.on('onNavigationComplete', (payload) =>
|
|
151
|
+
events.push(`complete:${payload.ready}`)
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const ready = await manager.navigate({
|
|
155
|
+
url: 'https://example.com',
|
|
156
|
+
timeout: 1000,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
assert.strictEqual(ready, false);
|
|
160
|
+
assert.ok(!events.includes('pageReady'));
|
|
161
|
+
assert.ok(events.includes('complete:false'));
|
|
162
|
+
assert.strictEqual(manager.isNavigating(), false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should emit onPageReady exactly once when every check passes', async () => {
|
|
166
|
+
const { manager } = createManagerWithNetwork({ idle: true });
|
|
167
|
+
const events = [];
|
|
168
|
+
manager.on('onPageReady', () => events.push('pageReady'));
|
|
169
|
+
|
|
170
|
+
const result = await manager.waitForReady({ timeout: 2000 });
|
|
171
|
+
|
|
172
|
+
assert.strictEqual(result.ready, true);
|
|
173
|
+
assert.strictEqual(result.status, READINESS_STATUS.READY);
|
|
174
|
+
assert.deepStrictEqual(events, ['pageReady']);
|
|
175
|
+
assert.ok(result.checks.satisfied.includes('networkIdleFor'));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should record evidence for every check it ran', async () => {
|
|
179
|
+
const { manager } = createManagerWithNetwork({ idle: true });
|
|
180
|
+
|
|
181
|
+
const result = await manager.waitForReady({ timeout: 2000 });
|
|
182
|
+
|
|
183
|
+
assert.ok(Array.isArray(result.evidence));
|
|
184
|
+
assert.ok(result.evidence.length >= 2);
|
|
185
|
+
for (const entry of result.evidence) {
|
|
186
|
+
assert.strictEqual(typeof entry.name, 'string');
|
|
187
|
+
assert.strictEqual(typeof entry.satisfied, 'boolean');
|
|
188
|
+
assert.strictEqual(typeof entry.elapsedMs, 'number');
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should stop running checks once the deadline is exhausted', async () => {
|
|
193
|
+
let secondCheckRan = false;
|
|
194
|
+
const { manager } = createManagerWithNetwork({ idle: true });
|
|
195
|
+
|
|
196
|
+
const slow = {
|
|
197
|
+
name: 'slow',
|
|
198
|
+
async run({ deadline }) {
|
|
199
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
200
|
+
return {
|
|
201
|
+
satisfied: false,
|
|
202
|
+
detail: { remaining: deadline.remainingMs() },
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
const second = {
|
|
207
|
+
name: 'second',
|
|
208
|
+
async run() {
|
|
209
|
+
secondCheckRan = true;
|
|
210
|
+
return { satisfied: true };
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const result = await manager.waitForReady({
|
|
215
|
+
timeout: 50,
|
|
216
|
+
checks: [slow, second],
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
assert.strictEqual(result.ready, false);
|
|
220
|
+
assert.strictEqual(secondCheckRan, false);
|
|
221
|
+
assert.ok(result.checks.pending.includes('second'));
|
|
222
|
+
});
|
|
223
|
+
});
|
|
62
224
|
});
|