browser-commander 0.28.0 → 0.29.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 (43) hide show
  1. package/.jscpd-baseline.json +5 -19
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +20 -10
  4. package/package.json +1 -1
  5. package/src/browser/browser-profile-files.js +5 -2
  6. package/src/browser/connector.js +140 -65
  7. package/src/browser/persistent-session.js +110 -38
  8. package/src/browser/session-adoption.js +96 -0
  9. package/src/browser/session-worker.js +12 -0
  10. package/src/cli/args.js +3 -0
  11. package/src/cli/commands.js +3 -0
  12. package/src/cli/high-level-methods.js +7 -0
  13. package/src/cli/sessions.js +3 -0
  14. package/src/core/commander-features.js +6 -1
  15. package/src/core/navigation-safety.js +1 -0
  16. package/src/core/page-trigger-manager.js +173 -134
  17. package/src/index.js +2 -0
  18. package/src/interactions/overlays.js +59 -14
  19. package/src/traces/bundle.js +41 -3
  20. package/src/traces/checkpoints.js +7 -3
  21. package/src/traces/links.js +44 -5
  22. package/src/traces/mutation-stream.js +8 -2
  23. package/src/traces/network.js +65 -13
  24. package/src/traces/page-capture.js +65 -21
  25. package/src/traces/recorder.js +33 -3
  26. package/src/traces/redaction.js +89 -6
  27. package/src/traces/rolling-bundle.js +240 -0
  28. package/src/traces/rolling.js +10 -113
  29. package/src/traces/stop.js +1 -0
  30. package/tests/e2e/issue-160.e2e.test.js +291 -0
  31. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +1 -1
  32. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +1 -1
  33. package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +2 -2
  34. package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +4 -0
  35. package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +1 -1
  36. package/tests/fixtures/traces/conformance/expected/trace.lino +2 -2
  37. package/tests/unit/browser/issue-160-connector.test.js +157 -0
  38. package/tests/unit/core/interrupted-navigation.test.js +32 -0
  39. package/tests/unit/core/trigger-stop-race.test.js +80 -0
  40. package/tests/unit/interactions/overlays.test.js +58 -0
  41. package/tests/unit/traces/issue-160-privacy.test.js +110 -0
  42. package/tests/unit/traces/issue-160-rotation.test.js +140 -0
  43. package/tests/unit/traces/network.test.js +55 -3
@@ -0,0 +1,140 @@
1
+ import { it } from 'node:test';
2
+ import { openTraceBundle } from '../../../src/traces/bundle.js';
3
+ import assert from 'node:assert/strict';
4
+ import fs from 'node:fs/promises';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { startTrace } from '../../../src/traces/recorder.js';
8
+ import { readTrace } from '../../../src/traces/reader.js';
9
+ import {
10
+ createFakePage,
11
+ createFakeCommander,
12
+ } from '../../helpers/trace-fixtures.js';
13
+
14
+ function assertUniqueIncreasingSequences(events) {
15
+ const sequences = events.map((event) => event.sequence);
16
+ assert.deepEqual(
17
+ sequences,
18
+ [...new Set(sequences)].sort((a, b) => a - b)
19
+ );
20
+ }
21
+
22
+ it('rotates before oversized checkpoints and retains every segment by default', async () => {
23
+ const output = await fs.mkdtemp(path.join(os.tmpdir(), 'bc-rotation-'));
24
+ const page = createFakePage({
25
+ snapshot: {
26
+ html: `<div>${'payload'.repeat(1500)}</div>`,
27
+ state: { url: 'https://example.test' },
28
+ },
29
+ });
30
+ try {
31
+ const trace = await startTrace({
32
+ commander: createFakeCommander(page),
33
+ output,
34
+ screenshots: false,
35
+ links: { output: path.join(output, 'trace.lino'), dom: 'full' },
36
+ limits: { rotate: { maxBytes: 2048 } },
37
+ });
38
+ assert.equal(trace.links, path.join(output, 'trace.lino'));
39
+ for (let i = 0; i < 8; i++) {
40
+ await trace.checkpoint(`snapshot-${i}`);
41
+ await trace.event('step', { payload: 'x'.repeat(1500), i });
42
+ }
43
+ await trace.stop();
44
+ assert.equal(trace.stopped, true);
45
+ const opened = await readTrace(trace.path);
46
+ assert.equal(opened.events.filter((e) => e.action === 'step').length, 8);
47
+ assert.equal(opened.events.filter((e) => e.kind === 'dropped').length, 0);
48
+ assert.ok(opened.checkpoints.length >= 8);
49
+ assert.match(await opened.html(opened.checkpoints[0].index), /payload/);
50
+ assert.match(await fs.readFile(trace.links, 'utf8'), /dom-snapshot/);
51
+ assert.ok(
52
+ JSON.parse(await fs.readFile(path.join(output, 'segments.json'), 'utf8'))
53
+ .segments.length > 4
54
+ );
55
+ } finally {
56
+ await fs.rm(output, { recursive: true, force: true });
57
+ }
58
+ });
59
+
60
+ it('automatically rotates continuous traces instead of permanently dropping later events', async () => {
61
+ const output = await fs.mkdtemp(path.join(os.tmpdir(), 'bc-continuous-'));
62
+ try {
63
+ const trace = await startTrace({
64
+ commander: createFakeCommander(createFakePage()),
65
+ output,
66
+ mode: 'continuous',
67
+ screenshots: false,
68
+ limits: { maxBundleBytes: 2048 },
69
+ });
70
+ for (let i = 0; i < 12; i++) {
71
+ await trace.event('step', { i, payload: 'x'.repeat(1500) });
72
+ }
73
+ await trace.stop();
74
+ const opened = await readTrace(output);
75
+ assert.equal(opened.events.filter((e) => e.action === 'step').length, 12);
76
+ assert.equal(opened.events.filter((e) => e.kind === 'dropped').length, 0);
77
+ assertUniqueIncreasingSequences(opened.events);
78
+ } finally {
79
+ await fs.rm(output, { recursive: true, force: true });
80
+ }
81
+ });
82
+
83
+ it('caps mutation intervals with a marker and records the next interval', async () => {
84
+ const output = await fs.mkdtemp(path.join(os.tmpdir(), 'bc-interval-'));
85
+ const bundle = await openTraceBundle({
86
+ output,
87
+ limits: { maxMutationBytes: 512 },
88
+ });
89
+ try {
90
+ await bundle.writeMutations(1, [
91
+ { records: [{ kind: 'attribute', value: 'a' }] },
92
+ { records: [{ value: 'x'.repeat(1000) }] },
93
+ ]);
94
+ assert.equal(bundle.mutationTruncated, true);
95
+ assert.equal(await bundle.writeMutations(1, [{ records: [] }]), null);
96
+ const first = await fs.readFile(
97
+ path.join(output, 'mutations/0001.ndjson'),
98
+ 'utf8'
99
+ );
100
+ assert.ok(Buffer.byteLength(first) <= 512);
101
+ assert.match(first, /truncated/);
102
+ await bundle.writeMutations(2, [
103
+ { records: [{ kind: 'attribute', value: 'next' }] },
104
+ ]);
105
+ assert.equal(bundle.mutationTruncated, false);
106
+ assert.match(
107
+ await fs.readFile(path.join(output, 'mutations/0002.ndjson'), 'utf8'),
108
+ /next/
109
+ );
110
+ } finally {
111
+ await bundle.abort();
112
+ await fs.rm(output, { recursive: true, force: true });
113
+ }
114
+ });
115
+
116
+ it('keeps timeline sequences unique across drops and rotation', async () => {
117
+ const output = await fs.mkdtemp(path.join(os.tmpdir(), 'bc-drop-rotation-'));
118
+ try {
119
+ const trace = await startTrace({
120
+ commander: createFakeCommander(createFakePage()),
121
+ output,
122
+ mode: 'continuous',
123
+ initialCheckpoint: false,
124
+ screenshots: false,
125
+ limits: { maxBundleBytes: 4096, maxEventBytes: 1024 },
126
+ });
127
+ await trace.event('oversized', { payload: 'x'.repeat(2000) });
128
+ for (let i = 0; i < 8; i++) {
129
+ await trace.event('step', { i, payload: 'x'.repeat(300) });
130
+ }
131
+ await trace.stop();
132
+ const opened = await readTrace(output);
133
+ assert.equal(opened.events.filter((e) => e.kind === 'dropped').length, 1);
134
+ assert.equal(opened.events.filter((e) => e.action === 'step').length, 8);
135
+ assert.ok(trace.segments.length > 1);
136
+ assertUniqueIncreasingSequences(opened.events);
137
+ } finally {
138
+ await fs.rm(output, { recursive: true, force: true });
139
+ }
140
+ });
@@ -10,9 +10,15 @@ import {
10
10
  createFakePage,
11
11
  } from '../../helpers/trace-fixtures.js';
12
12
 
13
+ async function networkFixture() {
14
+ return {
15
+ output: await fs.mkdtemp(path.join(os.tmpdir(), 'commander-network-')),
16
+ page: createFakePage(),
17
+ };
18
+ }
19
+
13
20
  it('records filtered network metadata and bounded redacted bodies in HAR', async () => {
14
- const output = await fs.mkdtemp(path.join(os.tmpdir(), 'commander-network-'));
15
- const page = createFakePage();
21
+ const { output, page } = await networkFixture();
16
22
  try {
17
23
  const trace = await startTrace({
18
24
  commander: createFakeCommander(page),
@@ -55,7 +61,7 @@ it('records filtered network metadata and bounded redacted bodies in HAR', async
55
61
  await fs.readFile(path.join(output, 'network.har'), 'utf8')
56
62
  );
57
63
  assert.equal(har.log.entries.length, 1);
58
- assert.equal(har.log.entries[0].response.content.text, 'YWJjZA==');
64
+ assert.equal(har.log.entries[0].response.content.text, 'abcd');
59
65
  assert.equal(har.log.entries[0].response.content._truncated, true);
60
66
  } finally {
61
67
  await fs.rm(output, { recursive: true, force: true });
@@ -95,3 +101,49 @@ it('rejected video startup removes every installed observer', async () => {
95
101
  await fs.rm(output, { recursive: true, force: true });
96
102
  }
97
103
  });
104
+
105
+ it('omits binary bytes while preserving reported original size without buffering', async () => {
106
+ const { output, page } = await networkFixture();
107
+ try {
108
+ const trace = await startTrace({
109
+ commander: createFakeCommander(page),
110
+ output,
111
+ initialCheckpoint: false,
112
+ network: { bodies: true, har: true },
113
+ });
114
+ const request = {
115
+ url: () => 'https://example.test/binary',
116
+ method: () => 'GET',
117
+ resourceType: () => 'fetch',
118
+ headers: () => ({}),
119
+ sizes: async () => ({ responseBodySize: 1234 }),
120
+ };
121
+ page.emit('request', request);
122
+ page.emit('response', {
123
+ request: () => request,
124
+ status: () => 200,
125
+ headers: () => ({ 'content-type': 'application/octet-stream' }),
126
+ body: async () => {
127
+ assert.fail('binary body must not be buffered');
128
+ },
129
+ });
130
+ await trace.stop();
131
+ const events = (
132
+ await fs.readFile(path.join(output, 'events.ndjson'), 'utf8')
133
+ )
134
+ .trim()
135
+ .split('\n')
136
+ .map(JSON.parse);
137
+ assert.deepEqual(
138
+ events.find((event) => event.kind === 'network.response').body,
139
+ { size: 1234, omitted: 'binary' }
140
+ );
141
+ const har = JSON.parse(
142
+ await fs.readFile(path.join(output, 'network.har'), 'utf8')
143
+ );
144
+ assert.equal(har.log.entries[0].response.content.size, 1234);
145
+ assert.equal(har.log.entries[0].response.content.text, undefined);
146
+ } finally {
147
+ await fs.rm(output, { recursive: true, force: true });
148
+ }
149
+ });