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.
Files changed (35) hide show
  1. package/.jscpd-baseline.json +8 -0
  2. package/CHANGELOG.md +45 -0
  3. package/README.md +50 -2
  4. package/package.json +8 -3
  5. package/scripts/run-tests.mjs +4 -2
  6. package/src/downloads/manager.js +40 -8
  7. package/src/downloads/sources.js +127 -6
  8. package/src/downloads/staging.js +158 -0
  9. package/src/exports.js +14 -0
  10. package/src/traces/bundle.js +26 -3
  11. package/src/traces/deadline.js +33 -0
  12. package/src/traces/identity.js +127 -0
  13. package/src/traces/init-script.js +95 -0
  14. package/src/traces/links.js +623 -0
  15. package/src/traces/mutation-stream.js +232 -0
  16. package/src/traces/observers.js +29 -3
  17. package/src/traces/page-capture.js +251 -15
  18. package/src/traces/recorder.js +124 -94
  19. package/src/traces/schema.js +65 -1
  20. package/src/traces/viewer.js +157 -11
  21. package/tests/e2e/downloads.e2e.test.js +126 -2
  22. package/tests/e2e/traces.e2e.test.js +296 -11
  23. package/tests/helpers/download-server.js +26 -6
  24. package/tests/helpers/trace-fixtures.js +97 -3
  25. package/tests/helpers/trace-server.js +57 -1
  26. package/tests/unit/downloads/manager.test.js +213 -9
  27. package/tests/unit/downloads/staging.test.js +139 -0
  28. package/tests/unit/scripts/ci-workflow-policy.test.js +8 -0
  29. package/tests/unit/scripts/npm-release-helpers.test.js +3 -2
  30. package/tests/unit/scripts/pre-commit-config.test.js +2 -1
  31. package/tests/unit/traces/bundle.test.js +24 -3
  32. package/tests/unit/traces/links.test.js +564 -0
  33. package/tests/unit/traces/reader.test.js +10 -4
  34. package/tests/unit/traces/recorder.test.js +172 -1
  35. 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
- assert.strictEqual(installs.length, 2);
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 () => {