browser-commander 0.18.0 → 0.20.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 +8 -0
- package/CHANGELOG.md +45 -0
- package/README.md +50 -2
- package/package.json +8 -3
- package/scripts/run-tests.mjs +4 -2
- package/src/downloads/manager.js +40 -8
- package/src/downloads/sources.js +127 -6
- package/src/downloads/staging.js +158 -0
- package/src/exports.js +14 -0
- package/src/traces/bundle.js +26 -3
- 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 +29 -3
- package/src/traces/page-capture.js +251 -15
- package/src/traces/recorder.js +124 -94
- package/src/traces/schema.js +65 -1
- package/src/traces/viewer.js +157 -11
- package/tests/e2e/downloads.e2e.test.js +126 -2
- package/tests/e2e/traces.e2e.test.js +296 -11
- package/tests/helpers/download-server.js +26 -6
- package/tests/helpers/trace-fixtures.js +97 -3
- package/tests/helpers/trace-server.js +57 -1
- package/tests/unit/downloads/manager.test.js +213 -9
- package/tests/unit/downloads/staging.test.js +139 -0
- package/tests/unit/scripts/ci-workflow-policy.test.js +8 -0
- package/tests/unit/scripts/npm-release-helpers.test.js +3 -2
- package/tests/unit/scripts/pre-commit-config.test.js +2 -1
- package/tests/unit/traces/bundle.test.js +24 -3
- package/tests/unit/traces/links.test.js +564 -0
- package/tests/unit/traces/reader.test.js +10 -4
- package/tests/unit/traces/recorder.test.js +172 -1
- package/tests/unit/traces/viewer.test.js +9 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
+
import { setImmediate as yieldToEngine } from 'node:timers/promises';
|
|
3
4
|
import fs from 'node:fs/promises';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
|
|
@@ -7,6 +8,7 @@ import { startTrace } from '../../../src/traces/recorder.js';
|
|
|
7
8
|
import { readTrace } from '../../../src/traces/reader.js';
|
|
8
9
|
import { REDACTED } from '../../../src/traces/redaction.js';
|
|
9
10
|
import {
|
|
11
|
+
TRACE_CHECKPOINT_REASON,
|
|
10
12
|
TRACE_DROP_REASON,
|
|
11
13
|
TRACE_EVENT,
|
|
12
14
|
TRACE_MODE,
|
|
@@ -170,6 +172,7 @@ describe('trace recorder (issue #87)', () => {
|
|
|
170
172
|
await trace.checkpoint('start');
|
|
171
173
|
|
|
172
174
|
assert.strictEqual(page.state.screenshots, 0);
|
|
175
|
+
await trace.stop();
|
|
173
176
|
});
|
|
174
177
|
|
|
175
178
|
it('should take a screenshot only on failure when asked', async () => {
|
|
@@ -180,6 +183,7 @@ describe('trace recorder (issue #87)', () => {
|
|
|
180
183
|
|
|
181
184
|
await trace.checkpoint('the end', { reason: 'failure' });
|
|
182
185
|
assert.strictEqual(page.state.screenshots, 1);
|
|
186
|
+
await trace.stop();
|
|
183
187
|
});
|
|
184
188
|
|
|
185
189
|
it('should keep a gap rather than fail when the page has closed', async () => {
|
|
@@ -232,7 +236,10 @@ describe('trace recorder (issue #87)', () => {
|
|
|
232
236
|
const installs = page.state.evaluated.filter(
|
|
233
237
|
(call) => call.name === 'installMutationRecorderInPage'
|
|
234
238
|
);
|
|
235
|
-
|
|
239
|
+
// Once at the start, and once after each of the two checkpoints - the
|
|
240
|
+
// base one the recorder takes itself and the one named above.
|
|
241
|
+
assert.strictEqual(installs.length, 3);
|
|
242
|
+
await trace.stop();
|
|
236
243
|
});
|
|
237
244
|
});
|
|
238
245
|
|
|
@@ -272,6 +279,170 @@ describe('trace recorder (issue #87)', () => {
|
|
|
272
279
|
});
|
|
273
280
|
});
|
|
274
281
|
|
|
282
|
+
describe('continuity (issue #93)', () => {
|
|
283
|
+
it('should take a base snapshot a continuous trace can replay from', async () => {
|
|
284
|
+
const { trace } = await start({ mode: TRACE_MODE.CONTINUOUS });
|
|
285
|
+
const stopped = await trace.stop();
|
|
286
|
+
|
|
287
|
+
const [base] = stopped.checkpoints;
|
|
288
|
+
assert.strictEqual(base.name, 'initial');
|
|
289
|
+
assert.strictEqual(base.actor, 'recorder');
|
|
290
|
+
assert.strictEqual(base.reason, TRACE_CHECKPOINT_REASON.INITIAL);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should leave a checkpoints-only trace the moments its caller named', async () => {
|
|
294
|
+
const { trace } = await start({ mode: TRACE_MODE.CHECKPOINTS });
|
|
295
|
+
await trace.checkpoint('only this one');
|
|
296
|
+
const stopped = await trace.stop();
|
|
297
|
+
|
|
298
|
+
assert.deepStrictEqual(
|
|
299
|
+
stopped.checkpoints.map((entry) => entry.name),
|
|
300
|
+
['only this one']
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should let the caller name the base snapshot', async () => {
|
|
305
|
+
const { trace } = await start({
|
|
306
|
+
mode: TRACE_MODE.CONTINUOUS,
|
|
307
|
+
initialCheckpoint: 'before anything',
|
|
308
|
+
});
|
|
309
|
+
const stopped = await trace.stop();
|
|
310
|
+
|
|
311
|
+
assert.strictEqual(stopped.checkpoints[0].name, 'before anything');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should let the caller refuse the base snapshot', async () => {
|
|
315
|
+
const { trace } = await start({
|
|
316
|
+
mode: TRACE_MODE.CONTINUOUS,
|
|
317
|
+
initialCheckpoint: false,
|
|
318
|
+
});
|
|
319
|
+
const stopped = await trace.stop();
|
|
320
|
+
|
|
321
|
+
assert.deepStrictEqual(stopped.checkpoints, []);
|
|
322
|
+
const started = await eventOfKind(stopped, TRACE_EVENT.TRACE_START);
|
|
323
|
+
assert.strictEqual(started.initialCheckpoint, false);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should observe a document created after the trace started', async () => {
|
|
327
|
+
const { trace, page } = await start({ mode: TRACE_MODE.CONTINUOUS });
|
|
328
|
+
await trace.stop();
|
|
329
|
+
|
|
330
|
+
// Registered with the engine rather than evaluated: this is what runs
|
|
331
|
+
// before a freshly navigated document's own scripts do.
|
|
332
|
+
assert.deepStrictEqual(
|
|
333
|
+
page.state.initScripts.map((script) => script.name),
|
|
334
|
+
['installMutationRecorderInPage']
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should ask the page to record live control state', async () => {
|
|
339
|
+
const { trace, page } = await start({ mode: TRACE_MODE.CONTINUOUS });
|
|
340
|
+
await trace.stop();
|
|
341
|
+
|
|
342
|
+
const [installed] = page.state.evaluated.filter(
|
|
343
|
+
(call) => call.name === 'installMutationRecorderInPage'
|
|
344
|
+
);
|
|
345
|
+
assert.strictEqual(installed.argument.liveState, true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should stop the in-page recorder when the trace stops', async () => {
|
|
349
|
+
const { trace, page } = await start({ mode: TRACE_MODE.CONTINUOUS });
|
|
350
|
+
await trace.stop();
|
|
351
|
+
|
|
352
|
+
const stops = page.state.evaluated.filter(
|
|
353
|
+
(call) => call.name === 'stopMutationRecorderInPage'
|
|
354
|
+
);
|
|
355
|
+
assert.strictEqual(stops.length, 1);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should name the owner of every timeline record', async () => {
|
|
359
|
+
const { trace } = await start({ mode: TRACE_MODE.CONTINUOUS });
|
|
360
|
+
const stopped = await trace.stop();
|
|
361
|
+
|
|
362
|
+
const events = (await read(stopped)).events;
|
|
363
|
+
assert.ok(events.length > 0);
|
|
364
|
+
for (const event of events) {
|
|
365
|
+
assert.match(event.traceId, /^trace-\d+$/);
|
|
366
|
+
assert.match(event.pageId, /^page-\d+$/);
|
|
367
|
+
assert.match(event.navigationId, /^nav-\d+$/);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should name the action a recorded interaction belongs to', async () => {
|
|
372
|
+
const { trace, commander } = await start();
|
|
373
|
+
await commander.click('#submit');
|
|
374
|
+
const stopped = await trace.stop();
|
|
375
|
+
|
|
376
|
+
const interaction = await eventOfKind(stopped, TRACE_EVENT.INTERACTION);
|
|
377
|
+
assert.match(interaction.actionId, /^trace-\d+-action-1$/);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should separate what happened before a navigation from what happened after', async () => {
|
|
381
|
+
const { trace, page } = await start({ mode: TRACE_MODE.CONTINUOUS });
|
|
382
|
+
const before = await trace.checkpoint('before');
|
|
383
|
+
|
|
384
|
+
page.emit('framenavigated', {
|
|
385
|
+
url: () => 'https://example.com/next',
|
|
386
|
+
parentFrame: () => null,
|
|
387
|
+
});
|
|
388
|
+
await yieldToEngine();
|
|
389
|
+
|
|
390
|
+
const after = await trace.checkpoint('after');
|
|
391
|
+
const stopped = await trace.stop();
|
|
392
|
+
|
|
393
|
+
const events = (await read(stopped)).events;
|
|
394
|
+
const owner = (index) =>
|
|
395
|
+
events.find(
|
|
396
|
+
(event) =>
|
|
397
|
+
event.kind === TRACE_EVENT.CHECKPOINT && event.index === index
|
|
398
|
+
).navigationId;
|
|
399
|
+
assert.notStrictEqual(owner(before.index), owner(after.index));
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should drain every frame of a page, not only the main one', async () => {
|
|
403
|
+
const page = createFakePage({
|
|
404
|
+
mutations: [{ sequence: 1, at: 1, records: [] }],
|
|
405
|
+
frames: [{ mutations: [{ sequence: 1, at: 2, records: [] }] }],
|
|
406
|
+
});
|
|
407
|
+
const { trace } = await start({ page, mode: TRACE_MODE.CONTINUOUS });
|
|
408
|
+
const stopped = await trace.stop();
|
|
409
|
+
|
|
410
|
+
const batches = await (await read(stopped)).mutations(0);
|
|
411
|
+
// Each batch says which document it came from, so two frames that both
|
|
412
|
+
// numbered their batch 1 are still telling two different stories.
|
|
413
|
+
assert.deepStrictEqual(
|
|
414
|
+
batches.map((batch) => batch.frameId),
|
|
415
|
+
['main', 'child-1']
|
|
416
|
+
);
|
|
417
|
+
assert.deepStrictEqual(
|
|
418
|
+
batches.map((batch) => batch.mainFrame),
|
|
419
|
+
[true, false]
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('should say in the manifest what the bundle can actually replay', async () => {
|
|
424
|
+
const { trace } = await start({ mode: TRACE_MODE.CONTINUOUS });
|
|
425
|
+
const stopped = await trace.stop();
|
|
426
|
+
|
|
427
|
+
assert.deepStrictEqual(stopped.manifest.replay, {
|
|
428
|
+
checkpoints: true,
|
|
429
|
+
mutations: true,
|
|
430
|
+
childListPositions: true,
|
|
431
|
+
liveState: true,
|
|
432
|
+
identifiers: true,
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should admit a checkpoints-only bundle cannot replay the gaps', async () => {
|
|
437
|
+
const { trace } = await start({ mode: TRACE_MODE.CHECKPOINTS });
|
|
438
|
+
const stopped = await trace.stop();
|
|
439
|
+
|
|
440
|
+
assert.strictEqual(stopped.manifest.replay.mutations, false);
|
|
441
|
+
assert.strictEqual(stopped.manifest.replay.liveState, false);
|
|
442
|
+
assert.strictEqual(stopped.manifest.replay.checkpoints, true);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
275
446
|
describe('the shared timeline', () => {
|
|
276
447
|
/** A completed download, as the manager reports one. */
|
|
277
448
|
const COMPLETED_DOWNLOAD = Object.freeze({
|
|
@@ -41,6 +41,11 @@ describe('offline trace viewer (issue #87)', () => {
|
|
|
41
41
|
output: path.join(directory.path, name),
|
|
42
42
|
mode: TRACE_MODE.CONTINUOUS,
|
|
43
43
|
screenshots: false,
|
|
44
|
+
// These are tests about what the viewer does with the checkpoints it is
|
|
45
|
+
// given, so the checkpoints are exactly the ones each test names. The
|
|
46
|
+
// base snapshot a continuous trace takes on its own is covered by the
|
|
47
|
+
// recorder's suite (issue #93).
|
|
48
|
+
initialCheckpoint: false,
|
|
44
49
|
...rest,
|
|
45
50
|
});
|
|
46
51
|
return { trace, page };
|
|
@@ -277,6 +282,10 @@ describe('offline trace viewer (issue #87)', () => {
|
|
|
277
282
|
|
|
278
283
|
assert.match(body, /outcome truncated/);
|
|
279
284
|
assert.match(body, /the last thing it did|1 checkpoints/);
|
|
285
|
+
|
|
286
|
+
// The assertions model an interrupted process; this test process keeps
|
|
287
|
+
// running, so explicitly release the descriptor afterwards.
|
|
288
|
+
await trace.stop();
|
|
280
289
|
});
|
|
281
290
|
|
|
282
291
|
it('should refuse a directory that is not a bundle', async () => {
|