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.
Files changed (74) hide show
  1. package/.jscpd-baseline.json +12 -17
  2. package/CHANGELOG.md +68 -0
  3. package/README.md +132 -0
  4. package/eslint.config.js +5 -0
  5. package/package.json +5 -1
  6. package/src/bindings.js +13 -1
  7. package/src/browser/connector.js +15 -3
  8. package/src/browser/launcher.js +14 -2
  9. package/src/browser/navigation.js +63 -0
  10. package/src/core/dialog-manager.js +48 -0
  11. package/src/core/navigation-manager.js +83 -87
  12. package/src/core/navigation-readiness.js +133 -0
  13. package/src/core/readiness.js +478 -0
  14. package/src/downloads/attach.js +58 -0
  15. package/src/downloads/destination.js +125 -0
  16. package/src/downloads/manager.js +476 -0
  17. package/src/downloads/naming.js +246 -0
  18. package/src/downloads/sources.js +238 -0
  19. package/src/downloads/staging.js +146 -0
  20. package/src/downloads/store.js +256 -0
  21. package/src/exports.js +86 -0
  22. package/src/factory.js +55 -0
  23. package/src/interactions/click-activation.js +317 -0
  24. package/src/interactions/click-result.js +108 -0
  25. package/src/interactions/click.js +766 -352
  26. package/src/tests/index.js +75 -11
  27. package/src/tests/tracing.js +236 -0
  28. package/src/traces/bundle.js +338 -0
  29. package/src/traces/deadline.js +33 -0
  30. package/src/traces/identity.js +127 -0
  31. package/src/traces/init-script.js +95 -0
  32. package/src/traces/links.js +623 -0
  33. package/src/traces/mutation-stream.js +232 -0
  34. package/src/traces/observers.js +272 -0
  35. package/src/traces/page-capture.js +601 -0
  36. package/src/traces/reader.js +226 -0
  37. package/src/traces/recorder.js +454 -0
  38. package/src/traces/redaction.js +231 -0
  39. package/src/traces/schema.js +229 -0
  40. package/src/traces/viewer.js +448 -0
  41. package/tests/e2e/click-readiness.e2e.test.js +240 -0
  42. package/tests/e2e/downloads.e2e.test.js +235 -0
  43. package/tests/e2e/traces.e2e.test.js +742 -0
  44. package/tests/helpers/click-fixtures.js +62 -0
  45. package/tests/helpers/download-fixtures.js +125 -0
  46. package/tests/helpers/download-server.js +129 -0
  47. package/tests/helpers/e2e-browser.js +67 -0
  48. package/tests/helpers/file-modes.js +31 -0
  49. package/tests/helpers/fixture-server.js +56 -0
  50. package/tests/helpers/mocks.js +63 -66
  51. package/tests/helpers/readiness-server.js +96 -0
  52. package/tests/helpers/temp-directory.js +44 -0
  53. package/tests/helpers/trace-fixtures.js +274 -0
  54. package/tests/helpers/trace-server.js +173 -0
  55. package/tests/unit/core/dialog-manager.test.js +118 -10
  56. package/tests/unit/core/navigation-manager.test.js +162 -0
  57. package/tests/unit/core/readiness.test.js +517 -0
  58. package/tests/unit/downloads/attach.test.js +220 -0
  59. package/tests/unit/downloads/destination.test.js +114 -0
  60. package/tests/unit/downloads/manager.test.js +485 -0
  61. package/tests/unit/downloads/naming.test.js +174 -0
  62. package/tests/unit/downloads/staging.test.js +117 -0
  63. package/tests/unit/downloads/store.test.js +215 -0
  64. package/tests/unit/interactions/click-activation.test.js +283 -0
  65. package/tests/unit/interactions/click-result.test.js +90 -0
  66. package/tests/unit/interactions/click.test.js +409 -200
  67. package/tests/unit/tests/tracing.test.js +299 -0
  68. package/tests/unit/traces/bundle.test.js +260 -0
  69. package/tests/unit/traces/links.test.js +557 -0
  70. package/tests/unit/traces/reader.test.js +208 -0
  71. package/tests/unit/traces/recorder.test.js +730 -0
  72. package/tests/unit/traces/redaction.test.js +170 -0
  73. package/tests/unit/traces/schema.test.js +92 -0
  74. package/tests/unit/traces/viewer.test.js +294 -0
@@ -0,0 +1,208 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ import { openTraceBundle } from '../../../src/traces/bundle.js';
7
+ import {
8
+ diffControlState,
9
+ parseNdjson,
10
+ readTrace,
11
+ } from '../../../src/traces/reader.js';
12
+ import {
13
+ createManifest,
14
+ TRACE_EVENT,
15
+ TRACE_FILES,
16
+ TRACE_MODE,
17
+ TRACE_OUTCOME,
18
+ } from '../../../src/traces/schema.js';
19
+ import { useTempTraceDirectory } from '../../helpers/trace-fixtures.js';
20
+
21
+ describe('trace reader (issue #87)', () => {
22
+ const directory = useTempTraceDirectory();
23
+
24
+ /**
25
+ * Write a small bundle the tests can read back.
26
+ *
27
+ * @param {Object} [options] - `{close}`; a bundle that is not closed has no
28
+ * manifest, exactly like a run that was killed
29
+ * @returns {Promise<Object>} The bundle writer
30
+ */
31
+ const writeBundle = async ({ close = true } = {}) => {
32
+ const bundle = await openTraceBundle({
33
+ output: path.join(directory.path, 'run'),
34
+ });
35
+ await bundle.appendEvent({
36
+ kind: TRACE_EVENT.TRACE_START,
37
+ mode: TRACE_MODE.CHECKPOINTS,
38
+ engine: 'playwright',
39
+ events: ['console'],
40
+ });
41
+ const members = await bundle.writeCheckpoint({
42
+ index: 1,
43
+ html: '<html><body>one</body></html>',
44
+ state: {
45
+ url: 'https://example.com/one',
46
+ controls: [{ path: 'input', value: 'before' }],
47
+ },
48
+ });
49
+ await bundle.appendEvent({
50
+ kind: TRACE_EVENT.CHECKPOINT,
51
+ index: 1,
52
+ name: 'start',
53
+ actor: 'automation',
54
+ reason: 'checkpoint',
55
+ url: 'https://example.com/one',
56
+ members,
57
+ });
58
+ await bundle.writeMutations(1, [{ records: [{ type: 'childList' }] }]);
59
+ if (close) {
60
+ await bundle.close(createManifest({ mode: TRACE_MODE.CHECKPOINTS }));
61
+ }
62
+ return bundle;
63
+ };
64
+
65
+ describe('parseNdjson', () => {
66
+ it('should keep everything before a half-written line', () => {
67
+ const parsed = parseNdjson('{"a":1}\n{"b":2}\n{"c":');
68
+
69
+ assert.deepStrictEqual(parsed.records, [{ a: 1 }, { b: 2 }]);
70
+ assert.strictEqual(parsed.truncated, true);
71
+ });
72
+
73
+ it('should read an empty body as an empty timeline', () => {
74
+ assert.deepStrictEqual(parseNdjson(null), {
75
+ records: [],
76
+ truncated: false,
77
+ });
78
+ });
79
+ });
80
+
81
+ describe('readTrace', () => {
82
+ it('should read the manifest, timeline and checkpoint members', async () => {
83
+ const bundle = await writeBundle();
84
+
85
+ const trace = await readTrace(bundle.root);
86
+
87
+ assert.strictEqual(trace.manifest.outcome, TRACE_OUTCOME.COMPLETE);
88
+ assert.strictEqual(trace.checkpoints.length, 1);
89
+ assert.strictEqual(trace.checkpoints[0].name, 'start');
90
+ assert.match(await trace.html(1), /one/);
91
+ assert.strictEqual((await trace.state(1)).url, 'https://example.com/one');
92
+ assert.strictEqual((await trace.mutations(1)).length, 1);
93
+ assert.strictEqual(trace.truncated, false);
94
+ });
95
+
96
+ it('should return nothing for a checkpoint member that was dropped', async () => {
97
+ const bundle = await writeBundle();
98
+
99
+ const trace = await readTrace(bundle.root);
100
+
101
+ assert.strictEqual(await trace.html(2), null);
102
+ assert.strictEqual(await trace.state(2), null);
103
+ assert.deepStrictEqual(await trace.mutations(2), []);
104
+ });
105
+
106
+ it('should rebuild a manifest for a run that never stopped', async () => {
107
+ const bundle = await writeBundle({ close: false });
108
+
109
+ const trace = await readTrace(bundle.root);
110
+
111
+ assert.strictEqual(trace.manifest.outcome, TRACE_OUTCOME.TRUNCATED);
112
+ assert.strictEqual(trace.manifest.engine, 'playwright');
113
+ assert.strictEqual(trace.manifest.counts.checkpoints, 1);
114
+ assert.strictEqual(trace.truncated, true);
115
+ assert.match(await trace.html(1), /one/);
116
+ });
117
+
118
+ it('should read a timeline whose last line was cut off', async () => {
119
+ const bundle = await writeBundle({ close: false });
120
+ await fs.appendFile(
121
+ path.join(bundle.root, TRACE_FILES.EVENTS),
122
+ '{"kind":"console","text":"half'
123
+ );
124
+
125
+ const trace = await readTrace(bundle.root);
126
+
127
+ assert.strictEqual(trace.truncated, true);
128
+ assert.strictEqual(trace.events.length, 2);
129
+ });
130
+
131
+ it('should refuse a directory that holds no trace', async () => {
132
+ await assert.rejects(
133
+ readTrace(path.join(directory.path, 'nothing-here')),
134
+ /no trace bundle at/
135
+ );
136
+ });
137
+
138
+ it('should refuse a manifest that is not readable JSON', async () => {
139
+ const bundle = await writeBundle();
140
+ await fs.writeFile(
141
+ path.join(bundle.root, TRACE_FILES.MANIFEST),
142
+ 'not json'
143
+ );
144
+
145
+ await assert.rejects(readTrace(bundle.root), /is not readable JSON/);
146
+ });
147
+
148
+ it('should refuse a bundle written by a newer format', async () => {
149
+ const bundle = await writeBundle();
150
+ await fs.writeFile(
151
+ path.join(bundle.root, TRACE_FILES.MANIFEST),
152
+ JSON.stringify({
153
+ format: 'browser-commander-trace',
154
+ schemaVersion: 99,
155
+ })
156
+ );
157
+
158
+ await assert.rejects(readTrace(bundle.root), /newer than this reader/);
159
+ });
160
+ });
161
+
162
+ describe('diffControlState', () => {
163
+ it('should report what a step changed, added and removed', () => {
164
+ const changes = diffControlState(
165
+ {
166
+ controls: [
167
+ { path: 'input#name', value: 'before' },
168
+ { path: 'input#gone', value: 'x' },
169
+ { path: 'input#same', value: 'stable' },
170
+ ],
171
+ },
172
+ {
173
+ controls: [
174
+ { path: 'input#name', value: 'after' },
175
+ { path: 'input#same', value: 'stable' },
176
+ { path: 'input#new', value: 'fresh' },
177
+ ],
178
+ }
179
+ );
180
+
181
+ assert.deepStrictEqual(changes, [
182
+ {
183
+ path: 'input#name',
184
+ change: 'changed',
185
+ before: 'before',
186
+ after: 'after',
187
+ },
188
+ { path: 'input#new', change: 'added', after: 'fresh' },
189
+ { path: 'input#gone', change: 'removed', before: 'x' },
190
+ ]);
191
+ });
192
+
193
+ it('should report a checkbox by what it is checked to', () => {
194
+ const changes = diffControlState(
195
+ { controls: [{ path: 'input', checked: false, value: 'on' }] },
196
+ { controls: [{ path: 'input', checked: true, value: 'on' }] }
197
+ );
198
+
199
+ assert.deepStrictEqual(changes, [
200
+ { path: 'input', change: 'changed', before: false, after: true },
201
+ ]);
202
+ });
203
+
204
+ it('should read a missing state as no controls at all', () => {
205
+ assert.deepStrictEqual(diffControlState(null, null), []);
206
+ });
207
+ });
208
+ });