browser-commander 0.29.0 → 0.30.1
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 +2 -1
- package/CHANGELOG.md +12 -0
- package/README.md +7 -0
- package/package.json +2 -2
- package/scripts/run-tests.mjs +3 -1
- package/src/browser/launcher.js +19 -8
- package/src/core/commander-features.js +27 -24
- package/src/traces/commander-actions.js +146 -0
- package/src/traces/debug-session.js +119 -0
- package/src/traces/drain-timer.js +25 -0
- package/src/traces/mutation-stream.js +4 -3
- package/src/traces/observers.js +50 -46
- package/src/traces/recorder.js +46 -25
- package/src/traces/user-actions.js +405 -0
- package/src/utilities/subprocess.js +1 -1
- package/tests/e2e/capture.e2e.test.js +96 -0
- package/tests/e2e/issue-162-recording.e2e.test.js +175 -0
- package/tests/e2e/traces.e2e.test.js +4 -1
- package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +2 -2
- package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +1 -1
- package/tests/fixtures/traces/conformance/expected/trace.lino +2 -2
- package/tests/fixtures/traces/conformance/scenario.json +1 -0
- package/tests/helpers/e2e-browser.js +2 -1
- package/tests/unit/traces/issue-162-defaults.test.js +223 -0
- package/tests/unit/traces/issue-162-user-actions.test.js +181 -0
- package/tests/unit/traces/recorder.test.js +4 -1
- package/tests/unit/utilities/subprocess.test.js +80 -2
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import http from 'node:http';
|
|
7
|
+
import { makeBrowserCommander } from '../../src/factory.js';
|
|
8
|
+
import { readTrace } from '../../src/traces/reader.js';
|
|
9
|
+
import { launchE2EBrowser } from '../helpers/e2e-browser.js';
|
|
10
|
+
|
|
11
|
+
async function activateWithCommander(page, commander) {
|
|
12
|
+
await commander.click({ selector: '#save', waitAfter: false, verify: false });
|
|
13
|
+
await page.focus('#typed');
|
|
14
|
+
await commander.pressKey({ key: 'Enter' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const engine of ['playwright', 'puppeteer']) {
|
|
18
|
+
it(
|
|
19
|
+
`records a person's actions and their failed request with ${engine}`,
|
|
20
|
+
{
|
|
21
|
+
skip: process.env.RUN_E2E !== 'true',
|
|
22
|
+
timeout: 60000,
|
|
23
|
+
},
|
|
24
|
+
async () => {
|
|
25
|
+
const output = await fs.mkdtemp(
|
|
26
|
+
path.join(os.tmpdir(), 'bc-user-actions-')
|
|
27
|
+
);
|
|
28
|
+
const server = http.createServer((request, response) => {
|
|
29
|
+
if (request.url === '/delete') {
|
|
30
|
+
response.writeHead(500, { 'content-type': 'application/json' });
|
|
31
|
+
response.end('{"error":"private response body"}');
|
|
32
|
+
} else {
|
|
33
|
+
response.setHeader('content-type', 'text/html');
|
|
34
|
+
response.end(`<!doctype html><form id="form" onsubmit="event.preventDefault()">
|
|
35
|
+
<button id="delete" type="button" aria-label="Delete position">Delete</button>
|
|
36
|
+
<input id="typed"><textarea id="notes"></textarea>
|
|
37
|
+
<button id="save">Save</button></form><div id="result"></div>
|
|
38
|
+
<div id="shadow"></div><iframe srcdoc='<button id="inside">Inside frame</button>'></iframe>
|
|
39
|
+
<script>
|
|
40
|
+
document.querySelector('#delete').onclick = async () => {
|
|
41
|
+
await fetch('/delete', { method: 'DELETE' });
|
|
42
|
+
document.querySelector('#result').textContent = 'Something went wrong on our end';
|
|
43
|
+
};
|
|
44
|
+
document.querySelector('#shadow').attachShadow({mode:'open'}).innerHTML = '<button>Shadow action</button>';
|
|
45
|
+
</script>`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
49
|
+
let launched, commander;
|
|
50
|
+
try {
|
|
51
|
+
launched = await launchE2EBrowser({ engine });
|
|
52
|
+
const { page } = launched;
|
|
53
|
+
const debugEnvironment = process.env.BROWSER_COMMANDER_DEBUG;
|
|
54
|
+
delete process.env.BROWSER_COMMANDER_DEBUG;
|
|
55
|
+
try {
|
|
56
|
+
commander = makeBrowserCommander({
|
|
57
|
+
page,
|
|
58
|
+
debug: { output },
|
|
59
|
+
enableNavigationManager: false,
|
|
60
|
+
});
|
|
61
|
+
await commander.ready;
|
|
62
|
+
} finally {
|
|
63
|
+
if (debugEnvironment !== undefined) {
|
|
64
|
+
process.env.BROWSER_COMMANDER_DEBUG = debugEnvironment;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
await page.goto(`http://127.0.0.1:${server.address().port}/`);
|
|
68
|
+
if (engine === 'playwright') {
|
|
69
|
+
await page.locator('#shadow').getByRole('button').click();
|
|
70
|
+
} else {
|
|
71
|
+
await page.click('#shadow >>> button');
|
|
72
|
+
}
|
|
73
|
+
const frame = page.frames().find((item) => item !== page.mainFrame());
|
|
74
|
+
await frame.click('#inside');
|
|
75
|
+
// Engine input is trusted browser input, just as a physical click is.
|
|
76
|
+
await page.click('#delete');
|
|
77
|
+
await page.waitForFunction(() =>
|
|
78
|
+
document.querySelector('#result').textContent.includes('wrong')
|
|
79
|
+
);
|
|
80
|
+
await page.click('#typed');
|
|
81
|
+
await page.keyboard.type('PRIVATE_TYPED_SENTINEL');
|
|
82
|
+
await page.keyboard.press('Escape');
|
|
83
|
+
await page.click('#save');
|
|
84
|
+
await page.click('#delete', { button: 'right' });
|
|
85
|
+
await page.evaluate(() =>
|
|
86
|
+
document
|
|
87
|
+
.querySelector('#delete')
|
|
88
|
+
.dispatchEvent(new window.MouseEvent('click', { bubbles: true }))
|
|
89
|
+
);
|
|
90
|
+
await page.evaluate(
|
|
91
|
+
() =>
|
|
92
|
+
(document.querySelector('#notes').value =
|
|
93
|
+
'PRIVATE_TEXTAREA_SENTINEL')
|
|
94
|
+
);
|
|
95
|
+
await page.click('#save');
|
|
96
|
+
await activateWithCommander(page, commander);
|
|
97
|
+
await commander.destroy();
|
|
98
|
+
const trace = await readTrace(output),
|
|
99
|
+
actions = trace.events.filter(
|
|
100
|
+
(event) => event.kind === 'interaction'
|
|
101
|
+
);
|
|
102
|
+
const clicks = actions.filter(
|
|
103
|
+
(event) =>
|
|
104
|
+
event.source === 'user' &&
|
|
105
|
+
event.action === 'click' &&
|
|
106
|
+
event.target.path === '#delete'
|
|
107
|
+
);
|
|
108
|
+
assert.equal(clicks.length, 1, 'synthetic events must not be recorded');
|
|
109
|
+
assert.equal(clicks[0].target.role, 'button');
|
|
110
|
+
assert.equal(clicks[0].target.name, 'Delete position');
|
|
111
|
+
assert.equal(clicks[0].target.text, 'Delete');
|
|
112
|
+
assert.ok(
|
|
113
|
+
actions.some(
|
|
114
|
+
(event) => event.source === 'user' && event.action === 'pointerdown'
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
assert.ok(
|
|
118
|
+
actions.some(
|
|
119
|
+
(event) =>
|
|
120
|
+
event.source === 'user' && event.target.name === 'Shadow action'
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
assert.ok(
|
|
124
|
+
actions.some(
|
|
125
|
+
(event) =>
|
|
126
|
+
event.source === 'user' &&
|
|
127
|
+
event.target.name === 'Inside frame' &&
|
|
128
|
+
event.mainFrame === false
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
assert.ok(
|
|
132
|
+
actions.some(
|
|
133
|
+
(event) => event.action === 'keydown' && event.key === 'Escape'
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
assert.ok(actions.some((event) => event.action === 'submit'));
|
|
137
|
+
assert.ok(actions.some((event) => event.action === 'contextmenu'));
|
|
138
|
+
assert.equal(
|
|
139
|
+
actions.filter((event) => event.source === 'commander').length,
|
|
140
|
+
2
|
|
141
|
+
);
|
|
142
|
+
assert.equal(
|
|
143
|
+
actions.filter(
|
|
144
|
+
(event) =>
|
|
145
|
+
event.source === 'user' &&
|
|
146
|
+
event.action === 'click' &&
|
|
147
|
+
event.target.path === '#save'
|
|
148
|
+
).length,
|
|
149
|
+
2
|
|
150
|
+
);
|
|
151
|
+
assert.doesNotMatch(
|
|
152
|
+
JSON.stringify(actions),
|
|
153
|
+
/PRIVATE_(TYPED|TEXTAREA)_SENTINEL/
|
|
154
|
+
);
|
|
155
|
+
const failed = trace.events.find(
|
|
156
|
+
(event) => event.kind === 'network.response' && event.status === 500
|
|
157
|
+
),
|
|
158
|
+
links = await fs.readFile(path.join(output, 'trace.lino'), 'utf8');
|
|
159
|
+
assert.equal(failed.method, 'DELETE');
|
|
160
|
+
assert.equal(failed.body, undefined, 'network bodies remain opt-in');
|
|
161
|
+
assert.ok(clicks[0].sequence < failed.sequence);
|
|
162
|
+
assert.match(links, /source: user/);
|
|
163
|
+
assert.match(links, /dom-snapshot/);
|
|
164
|
+
assert.match(links, /dom-mutation/);
|
|
165
|
+
assert.match(links, /Something went wrong on our end/);
|
|
166
|
+
assert.match(links, /status: 500/);
|
|
167
|
+
} finally {
|
|
168
|
+
await commander?.destroy();
|
|
169
|
+
await launched?.cleanup();
|
|
170
|
+
await new Promise((resolve) => server.close(resolve));
|
|
171
|
+
await fs.rm(output, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -65,7 +65,7 @@ function describeTraces(engine) {
|
|
|
65
65
|
engine,
|
|
66
66
|
downloadDirectory,
|
|
67
67
|
}));
|
|
68
|
-
commander = makeBrowserCommander({ page, downloads });
|
|
68
|
+
commander = makeBrowserCommander({ page, downloads, debug: false });
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
beforeEach(async () => {
|
|
@@ -91,6 +91,9 @@ function describeTraces(engine) {
|
|
|
91
91
|
commander.startTrace({
|
|
92
92
|
output: path.join(artifacts, `run-${++runs}`),
|
|
93
93
|
screenshots: false,
|
|
94
|
+
mode: 'checkpoints',
|
|
95
|
+
network: false,
|
|
96
|
+
links: false,
|
|
94
97
|
...options,
|
|
95
98
|
});
|
|
96
99
|
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
{"sequence":7,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"requestfailed","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","url":"https://api.example.com/v1?api_key=[redacted]&page=2","method":"POST","failure":"net::ERR_FAILED"}
|
|
8
8
|
{"sequence":8,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"dialog","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","type":"confirm","message":"Continue? #yes"}
|
|
9
9
|
{"sequence":9,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"download","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","phase":"completed","id":"download-1","suggestedFilename":"report final.csv","path":"/downloads/report final.csv","checksum":"sha256:00ff","bytes":42,"url":"https://files.example.com/r.csv?signature=[redacted]","failure":null}
|
|
10
|
-
{"sequence":10,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"interaction","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","actionId":"trace-1-action-1","action":"goto","target":"https://example.com/next?code=abc","durationMs":0,"ok":true}
|
|
11
|
-
{"sequence":11,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"interaction","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","actionId":"trace-1-action-2","action":"clickButton","target":"#go","durationMs":0,"ok":false,"error":"button #go is not visible"}
|
|
10
|
+
{"sequence":10,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"interaction","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","actionId":"trace-1-action-1","source":"commander","action":"goto","target":"https://example.com/next?code=abc","durationMs":0,"ok":true}
|
|
11
|
+
{"sequence":11,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"interaction","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","actionId":"trace-1-action-2","source":"commander","action":"clickButton","target":"#go","durationMs":0,"ok":false,"error":"button #go is not visible"}
|
|
12
12
|
{"sequence":12,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"dropped","reason":"size-limit","member":"mutations","detail":"2 records over the in-page queue limit"}
|
|
13
13
|
{"sequence":13,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"mutations","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","member":"mutations/0001.ndjson","batches":2,"checkpoint":1,"frames":1,"truncated":false}
|
|
14
14
|
{"sequence":14,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"checkpoint","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","index":2,"name":"after click","actor":"automation","reason":"checkpoint","url":"https://example.com/next?token=[redacted]","truncated":true,"members":{"html":"checkpoints/0002.html","state":"checkpoints/0002.state.json","screenshot":"checkpoints/0002.png"}}
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
<div class="panel" id="details"></div>
|
|
54
54
|
<div class="panel" id="diff"></div>
|
|
55
55
|
</main>
|
|
56
|
-
<script id="trace-data" type="application/json">{"manifest":{"schemaVersion":2,"format":"browser-commander-trace","mode":"continuous","outcome":"partial","startedAt":"2026-01-01T00:00:00.000Z","stoppedAt":"2026-01-01T00:00:00.000Z","commanderVersion":"0.0.0-conformance","engine":"playwright","browser":null,"platform":"conformance","runtime":"conformance","events":["navigation","interaction","console","pageerror","dialog","requestfailed","download"],"dom":{"html":true,"liveControlState":true,"liveState":true,"mutations":true,"openShadowRoots":true},"replay":{"checkpoints":true,"mutations":true,"childListPositions":true,"liveState":true,"identifiers":true},"privacy":{"redactSelectors":["input[type=password]","input[type=hidden]","[data-private]","[data-bc-redact]"],"redactAttributes":["authorization","proxy-authorization","cookie","set-cookie","x-api-key","x-auth-token","x-csrf-token"],"redactQueryParams":["access_token","api_key","apikey","auth","code","id_token","password","refresh_token","secret","session","signature","token","csrf","xsrf","_xsrf","ticket"],"hasCallback":false},"limits":{"maxQueuedMutations":100},"counts":{"checkpoints":3,"events":18,"mutationBatches":3},"dropped":1},"events":[{"sequence":1,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"trace.start","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-1","mode":"continuous","engine":"playwright","dom":{"html":true,"liveControlState":true,"liveState":true,"mutations":true,"openShadowRoots":true},"events":["navigation","interaction","console","pageerror","dialog","requestfailed","download"],"initialCheckpoint":true},{"sequence":2,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"checkpoint","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-1","index":1,"name":"initial","actor":"recorder","reason":"initial","url":"https://[redacted]:[redacted]@example.com/start?ticket=[redacted]&lang=en#access_token=[redacted]&view=1","truncated":false,"members":{"html":"checkpoints/0001.html","state":"checkpoints/0001.state.json","screenshot":"checkpoints/0001.png"}},{"sequence":3,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"navigation","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","phase":"framenavigated","mainFrame":true,"url":"https://example.com/next?token=[redacted]&q=a+b"},{"sequence":4,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"navigation","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","phase":"framenavigated","mainFrame":false,"url":"https://ads.example.com/frame"},{"sequence":5,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"console","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","level":"log","text":"key [redacted] loaded\n\ttab \"and\" 'quotes'"},{"sequence":6,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"pageerror","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","message":"boom: (unbalanced","stack":"Error: boom\n at main.js:1:2"},{"sequence":7,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"requestfailed","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","url":"https://api.example.com/v1?api_key=[redacted]&page=2","method":"POST","failure":"net::ERR_FAILED"},{"sequence":8,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"dialog","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","type":"confirm","message":"Continue? #yes"},{"sequence":9,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"download","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","phase":"completed","id":"download-1","suggestedFilename":"report final.csv","path":"/downloads/report final.csv","checksum":"sha256:00ff","bytes":42,"url":"https://files.example.com/r.csv?signature=[redacted]","failure":null},{"sequence":10,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"interaction","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","actionId":"trace-1-action-1","action":"goto","target":"https://example.com/next?code=abc","durationMs":0,"ok":true},{"sequence":11,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"interaction","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","actionId":"trace-1-action-2","action":"clickButton","target":"#go","durationMs":0,"ok":false,"error":"button #go is not visible"},{"sequence":12,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"dropped","reason":"size-limit","member":"mutations","detail":"2 records over the in-page queue limit"},{"sequence":13,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"mutations","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","member":"mutations/0001.ndjson","batches":2,"checkpoint":1,"frames":1,"truncated":false},{"sequence":14,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"checkpoint","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","index":2,"name":"after click","actor":"automation","reason":"checkpoint","url":"https://example.com/next?token=[redacted]","truncated":true,"members":{"html":"checkpoints/0002.html","state":"checkpoints/0002.state.json","screenshot":"checkpoints/0002.png"}},{"sequence":15,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"interaction","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","action":"note","actor":"caller","authorization":"[redacted]","detail":"uses [redacted]","pageUrl":"https://example.com/?session=[redacted]","count":3,"ratio":0.25,"whole":2,"nested":{"list":[1,"two",null,true]}},{"sequence":16,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"mutations","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","member":"mutations/0002.ndjson","batches":1,"checkpoint":2,"frames":1,"truncated":false},{"sequence":17,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"checkpoint","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","index":3,"name":"final","actor":"caller","reason":"failure","url":"https://example.com/next?token=[redacted]","truncated":true,"members":{"html":"checkpoints/0003.html","state":"checkpoints/0003.state.json","screenshot":"checkpoints/0003.png"}},{"sequence":18,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"trace.stop","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","discarded":false}],"checkpoints":[{"index":1,"name":"initial","actor":"recorder","reason":"initial","url":"https://[redacted]:[redacted]@example.com/start?ticket=[redacted]&lang=en#access_token=[redacted]&view=1","at":"2026-01-01T00:00:00.000Z","sequence":2,"members":{"html":"checkpoints/0001.html","state":"checkpoints/0001.state.json","screenshot":"checkpoints/0001.png"}},{"index":2,"name":"after click","actor":"automation","reason":"checkpoint","url":"https://example.com/next?token=[redacted]","at":"2026-01-01T00:00:00.000Z","sequence":14,"members":{"html":"checkpoints/0002.html","state":"checkpoints/0002.state.json","screenshot":"checkpoints/0002.png"}},{"index":3,"name":"final","actor":"caller","reason":"failure","url":"https://example.com/next?token=[redacted]","at":"2026-01-01T00:00:00.000Z","sequence":17,"members":{"html":"checkpoints/0003.html","state":"checkpoints/0003.state.json","screenshot":"checkpoints/0003.png"}}],"html":{"1":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\u003chead\u003e\u003ctitle\u003eStart\u003c/title\u003e\u003c/head\u003e\u003cbody\u003e\u003cform\u003e\u003cinput name=\"user\" value=\"alice\"\u003e\u003cinput type=\"checkbox\" name=\"agree\"\u003e\u003cselect name=\"plan\"\u003e\u003coption\u003ea\u003c/option\u003e\u003c/select\u003e\u003c/form\u003e\u003c/body\u003e\u003c/html\u003e","2":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\u003cbody\u003e\u003cform\u003e\u003cinput name=\"user\" value=\"bob\"\u003e\u003ctextarea\u003e[redacted] note\u003c/textarea\u003e\u003c/form\u003e\u003c/body\u003e\u003c/html\u003e","3":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\u003cbody\u003e\u003cform\u003e\u003cinput name=\"user\" value=\"bob\"\u003e\u003ctextarea\u003e[redacted] note\u003c/textarea\u003e\u003c/form\u003e\u003c/body\u003e\u003c/html\u003e"},"state":{"1":{"url":"https://[redacted]:[redacted]@example.com/start?ticket=[redacted]&lang=en#access_token=[redacted]&view=1","title":"Start – “quoted” 'single' \\ back","readyState":"complete","scroll":{"x":0,"y":12.5},"viewport":{"width":1280,"height":720},"activeElement":null,"controls":[{"path":"form \u003e input:nth-of-type(1)","tag":"input","type":"text","name":"user","id":null,"focused":false,"value":"alice","redacted":false},{"path":"form \u003e input:nth-of-type(2)","tag":"input","type":"checkbox","name":"agree","id":null,"focused":false,"value":"on","checked":false,"redacted":false},{"path":"form \u003e select","tag":"select","type":null,"name":"plan","id":null,"focused":false,"value":"a","selected":[0],"redacted":false}],"frames":[],"name":"initial","actor":"recorder","reason":"initial"},"2":{"url":"https://example.com/next?token=[redacted]","title":"Next","readyState":"interactive","scroll":{"x":0,"y":0},"viewport":{"width":1280,"height":720},"activeElement":"form \u003e textarea","controls":[{"path":"form \u003e input:nth-of-type(1)","tag":"input","type":"text","name":"user","id":null,"focused":false,"value":"bob","redacted":false},{"path":"form \u003e input:nth-of-type(2)","tag":"input","type":"checkbox","name":"agree","id":null,"focused":false,"value":"on","checked":true,"redacted":false},{"path":"form \u003e textarea","tag":"textarea","type":null,"name":null,"id":"note","focused":true,"value":"[redacted] note","scroll":{"x":0,"y":3},"redacted":false}],"frames":[{"url":"https://ads.example.com/frame","crossOrigin":true}],"name":"after click","actor":"automation","reason":"checkpoint"},"3":{"url":"https://example.com/next?token=[redacted]","title":"Next","readyState":"interactive","scroll":{"x":0,"y":0},"viewport":{"width":1280,"height":720},"activeElement":"form \u003e textarea","controls":[{"path":"form \u003e input:nth-of-type(1)","tag":"input","type":"text","name":"user","id":null,"focused":false,"value":"bob","redacted":false},{"path":"form \u003e input:nth-of-type(2)","tag":"input","type":"checkbox","name":"agree","id":null,"focused":false,"value":"on","checked":true,"redacted":false},{"path":"form \u003e textarea","tag":"textarea","type":null,"name":null,"id":"note","focused":true,"value":"[redacted] note","scroll":{"x":0,"y":3},"redacted":false}],"frames":[{"url":"https://ads.example.com/frame","crossOrigin":true}],"name":"final","actor":"caller","reason":"failure"}},"mutations":{"1":[{"traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","frameId":"main","mainFrame":true,"at":1767225600100,"records":[{"type":"live-state","target":2,"property":"value","value":"bo"},{"type":"childList","target":1,"added":[{"id":9,"html":"\u003cp\u003enew\u2028line\u003c/p\u003e"}],"removed":[],"index":3}]},{"traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","frameId":"main","mainFrame":true,"at":1767225600200,"records":[{"type":"attributes","target":4,"name":"class","value":"busy"}]}],"2":[{"traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","frameId":"main","mainFrame":true,"at":1767225600300,"records":[{"type":"characterData","target":7,"value":"done ✓ 😀"}]}],"3":[]},"diffs":{"1":[],"2":[{"path":"form \u003e input:nth-of-type(1)","change":"changed","before":"alice","after":"bob"},{"path":"form \u003e input:nth-of-type(2)","change":"changed","before":false,"after":true},{"path":"form \u003e textarea","change":"added","after":"[redacted] note"},{"path":"form \u003e select","change":"removed","before":"a"}],"3":[]},"elided":[]}</script>
|
|
56
|
+
<script id="trace-data" type="application/json">{"manifest":{"schemaVersion":2,"format":"browser-commander-trace","mode":"continuous","outcome":"partial","startedAt":"2026-01-01T00:00:00.000Z","stoppedAt":"2026-01-01T00:00:00.000Z","commanderVersion":"0.0.0-conformance","engine":"playwright","browser":null,"platform":"conformance","runtime":"conformance","events":["navigation","interaction","console","pageerror","dialog","requestfailed","download"],"dom":{"html":true,"liveControlState":true,"liveState":true,"mutations":true,"openShadowRoots":true},"replay":{"checkpoints":true,"mutations":true,"childListPositions":true,"liveState":true,"identifiers":true},"privacy":{"redactSelectors":["input[type=password]","input[type=hidden]","[data-private]","[data-bc-redact]"],"redactAttributes":["authorization","proxy-authorization","cookie","set-cookie","x-api-key","x-auth-token","x-csrf-token"],"redactQueryParams":["access_token","api_key","apikey","auth","code","id_token","password","refresh_token","secret","session","signature","token","csrf","xsrf","_xsrf","ticket"],"hasCallback":false},"limits":{"maxQueuedMutations":100},"counts":{"checkpoints":3,"events":18,"mutationBatches":3},"dropped":1},"events":[{"sequence":1,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"trace.start","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-1","mode":"continuous","engine":"playwright","dom":{"html":true,"liveControlState":true,"liveState":true,"mutations":true,"openShadowRoots":true},"events":["navigation","interaction","console","pageerror","dialog","requestfailed","download"],"initialCheckpoint":true},{"sequence":2,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"checkpoint","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-1","index":1,"name":"initial","actor":"recorder","reason":"initial","url":"https://[redacted]:[redacted]@example.com/start?ticket=[redacted]&lang=en#access_token=[redacted]&view=1","truncated":false,"members":{"html":"checkpoints/0001.html","state":"checkpoints/0001.state.json","screenshot":"checkpoints/0001.png"}},{"sequence":3,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"navigation","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","phase":"framenavigated","mainFrame":true,"url":"https://example.com/next?token=[redacted]&q=a+b"},{"sequence":4,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"navigation","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","phase":"framenavigated","mainFrame":false,"url":"https://ads.example.com/frame"},{"sequence":5,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"console","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","level":"log","text":"key [redacted] loaded\n\ttab \"and\" 'quotes'"},{"sequence":6,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"pageerror","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","message":"boom: (unbalanced","stack":"Error: boom\n at main.js:1:2"},{"sequence":7,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"requestfailed","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","url":"https://api.example.com/v1?api_key=[redacted]&page=2","method":"POST","failure":"net::ERR_FAILED"},{"sequence":8,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"dialog","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","type":"confirm","message":"Continue? #yes"},{"sequence":9,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"download","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","phase":"completed","id":"download-1","suggestedFilename":"report final.csv","path":"/downloads/report final.csv","checksum":"sha256:00ff","bytes":42,"url":"https://files.example.com/r.csv?signature=[redacted]","failure":null},{"sequence":10,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"interaction","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","actionId":"trace-1-action-1","source":"commander","action":"goto","target":"https://example.com/next?code=abc","durationMs":0,"ok":true},{"sequence":11,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"interaction","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","actionId":"trace-1-action-2","source":"commander","action":"clickButton","target":"#go","durationMs":0,"ok":false,"error":"button #go is not visible"},{"sequence":12,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"dropped","reason":"size-limit","member":"mutations","detail":"2 records over the in-page queue limit"},{"sequence":13,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"mutations","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","member":"mutations/0001.ndjson","batches":2,"checkpoint":1,"frames":1,"truncated":false},{"sequence":14,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"checkpoint","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","index":2,"name":"after click","actor":"automation","reason":"checkpoint","url":"https://example.com/next?token=[redacted]","truncated":true,"members":{"html":"checkpoints/0002.html","state":"checkpoints/0002.state.json","screenshot":"checkpoints/0002.png"}},{"sequence":15,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"interaction","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","action":"note","actor":"caller","authorization":"[redacted]","detail":"uses [redacted]","pageUrl":"https://example.com/?session=[redacted]","count":3,"ratio":0.25,"whole":2,"nested":{"list":[1,"two",null,true]}},{"sequence":16,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"mutations","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","member":"mutations/0002.ndjson","batches":1,"checkpoint":2,"frames":1,"truncated":false},{"sequence":17,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"checkpoint","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","index":3,"name":"final","actor":"caller","reason":"failure","url":"https://example.com/next?token=[redacted]","truncated":true,"members":{"html":"checkpoints/0003.html","state":"checkpoints/0003.state.json","screenshot":"checkpoints/0003.png"}},{"sequence":18,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"trace.stop","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","discarded":false}],"checkpoints":[{"index":1,"name":"initial","actor":"recorder","reason":"initial","url":"https://[redacted]:[redacted]@example.com/start?ticket=[redacted]&lang=en#access_token=[redacted]&view=1","at":"2026-01-01T00:00:00.000Z","sequence":2,"members":{"html":"checkpoints/0001.html","state":"checkpoints/0001.state.json","screenshot":"checkpoints/0001.png"}},{"index":2,"name":"after click","actor":"automation","reason":"checkpoint","url":"https://example.com/next?token=[redacted]","at":"2026-01-01T00:00:00.000Z","sequence":14,"members":{"html":"checkpoints/0002.html","state":"checkpoints/0002.state.json","screenshot":"checkpoints/0002.png"}},{"index":3,"name":"final","actor":"caller","reason":"failure","url":"https://example.com/next?token=[redacted]","at":"2026-01-01T00:00:00.000Z","sequence":17,"members":{"html":"checkpoints/0003.html","state":"checkpoints/0003.state.json","screenshot":"checkpoints/0003.png"}}],"html":{"1":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\u003chead\u003e\u003ctitle\u003eStart\u003c/title\u003e\u003c/head\u003e\u003cbody\u003e\u003cform\u003e\u003cinput name=\"user\" value=\"alice\"\u003e\u003cinput type=\"checkbox\" name=\"agree\"\u003e\u003cselect name=\"plan\"\u003e\u003coption\u003ea\u003c/option\u003e\u003c/select\u003e\u003c/form\u003e\u003c/body\u003e\u003c/html\u003e","2":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\u003cbody\u003e\u003cform\u003e\u003cinput name=\"user\" value=\"bob\"\u003e\u003ctextarea\u003e[redacted] note\u003c/textarea\u003e\u003c/form\u003e\u003c/body\u003e\u003c/html\u003e","3":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\u003cbody\u003e\u003cform\u003e\u003cinput name=\"user\" value=\"bob\"\u003e\u003ctextarea\u003e[redacted] note\u003c/textarea\u003e\u003c/form\u003e\u003c/body\u003e\u003c/html\u003e"},"state":{"1":{"url":"https://[redacted]:[redacted]@example.com/start?ticket=[redacted]&lang=en#access_token=[redacted]&view=1","title":"Start – “quoted” 'single' \\ back","readyState":"complete","scroll":{"x":0,"y":12.5},"viewport":{"width":1280,"height":720},"activeElement":null,"controls":[{"path":"form \u003e input:nth-of-type(1)","tag":"input","type":"text","name":"user","id":null,"focused":false,"value":"alice","redacted":false},{"path":"form \u003e input:nth-of-type(2)","tag":"input","type":"checkbox","name":"agree","id":null,"focused":false,"value":"on","checked":false,"redacted":false},{"path":"form \u003e select","tag":"select","type":null,"name":"plan","id":null,"focused":false,"value":"a","selected":[0],"redacted":false}],"frames":[],"name":"initial","actor":"recorder","reason":"initial"},"2":{"url":"https://example.com/next?token=[redacted]","title":"Next","readyState":"interactive","scroll":{"x":0,"y":0},"viewport":{"width":1280,"height":720},"activeElement":"form \u003e textarea","controls":[{"path":"form \u003e input:nth-of-type(1)","tag":"input","type":"text","name":"user","id":null,"focused":false,"value":"bob","redacted":false},{"path":"form \u003e input:nth-of-type(2)","tag":"input","type":"checkbox","name":"agree","id":null,"focused":false,"value":"on","checked":true,"redacted":false},{"path":"form \u003e textarea","tag":"textarea","type":null,"name":null,"id":"note","focused":true,"value":"[redacted] note","scroll":{"x":0,"y":3},"redacted":false}],"frames":[{"url":"https://ads.example.com/frame","crossOrigin":true}],"name":"after click","actor":"automation","reason":"checkpoint"},"3":{"url":"https://example.com/next?token=[redacted]","title":"Next","readyState":"interactive","scroll":{"x":0,"y":0},"viewport":{"width":1280,"height":720},"activeElement":"form \u003e textarea","controls":[{"path":"form \u003e input:nth-of-type(1)","tag":"input","type":"text","name":"user","id":null,"focused":false,"value":"bob","redacted":false},{"path":"form \u003e input:nth-of-type(2)","tag":"input","type":"checkbox","name":"agree","id":null,"focused":false,"value":"on","checked":true,"redacted":false},{"path":"form \u003e textarea","tag":"textarea","type":null,"name":null,"id":"note","focused":true,"value":"[redacted] note","scroll":{"x":0,"y":3},"redacted":false}],"frames":[{"url":"https://ads.example.com/frame","crossOrigin":true}],"name":"final","actor":"caller","reason":"failure"}},"mutations":{"1":[{"traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","frameId":"main","mainFrame":true,"at":1767225600100,"records":[{"type":"live-state","target":2,"property":"value","value":"bo"},{"type":"childList","target":1,"added":[{"id":9,"html":"\u003cp\u003enew\u2028line\u003c/p\u003e"}],"removed":[],"index":3}]},{"traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","frameId":"main","mainFrame":true,"at":1767225600200,"records":[{"type":"attributes","target":4,"name":"class","value":"busy"}]}],"2":[{"traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","frameId":"main","mainFrame":true,"at":1767225600300,"records":[{"type":"characterData","target":7,"value":"done ✓ 😀"}]}],"3":[]},"diffs":{"1":[],"2":[{"path":"form \u003e input:nth-of-type(1)","change":"changed","before":"alice","after":"bob"},{"path":"form \u003e input:nth-of-type(2)","change":"changed","before":false,"after":true},{"path":"form \u003e textarea","change":"added","after":"[redacted] note"},{"path":"form \u003e select","change":"removed","before":"a"}],"3":[]},"elided":[]}</script>
|
|
57
57
|
<script>
|
|
58
58
|
const trace = JSON.parse(document.getElementById('trace-data').textContent);
|
|
59
59
|
const timeline = document.getElementById('timeline');
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
(timeline: (sequence: 7) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: requestfailed) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: browser) (target: 'https://api.example.com/v1?api_key=[redacted]&page=2') (outcome: failed) (url: 'https://api.example.com/v1?api_key=[redacted]&page=2') (method: POST) (failure: 'net::ERR_FAILED'))
|
|
10
10
|
(timeline: (sequence: 8) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: dialog) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: browser) (outcome: recorded) (type: confirm) (message: 'Continue? #yes'))
|
|
11
11
|
(timeline: (sequence: 9) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: download) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: browser) (action: completed) (target: 'https://files.example.com/r.csv?signature=[redacted]') (outcome: recorded) (phase: completed) (id: download-1) (suggestedFilename: 'report final.csv') (path: '/downloads/report final.csv') (checksum: 'sha256:00ff') (bytes: 42) (url: 'https://files.example.com/r.csv?signature=[redacted]'))
|
|
12
|
-
(timeline: (sequence: 10) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: interaction) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: automation) (action: goto) (target: 'https://example.com/next?code=abc') (outcome: ok) (actionId: trace-1-action-1) (durationMs: 0) (ok: true))
|
|
13
|
-
(timeline: (sequence: 11) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: interaction) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: automation) (action: clickButton) (target: '#go') (outcome: failed) (actionId: trace-1-action-2) (durationMs: 0) (ok: false) (error: 'button #go is not visible'))
|
|
12
|
+
(timeline: (sequence: 10) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: interaction) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: automation) (action: goto) (target: 'https://example.com/next?code=abc') (outcome: ok) (actionId: trace-1-action-1) (source: commander) (durationMs: 0) (ok: true))
|
|
13
|
+
(timeline: (sequence: 11) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: interaction) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: automation) (action: clickButton) (target: '#go') (outcome: failed) (actionId: trace-1-action-2) (source: commander) (durationMs: 0) (ok: false) (error: 'button #go is not visible'))
|
|
14
14
|
(timeline: (sequence: 12) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: dropped) (actor: recorder) (target: mutations) (outcome: dropped) (reason: size-limit) (member: mutations) (detail: '2 records over the in-page queue limit'))
|
|
15
15
|
(timeline: (sequence: 13) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: mutations) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: recorder) (target: mutations/0001.ndjson) (outcome: recorded) (member: mutations/0001.ndjson) (batches: 2) (checkpoint: 1) (frames: 1) (truncated: false))
|
|
16
16
|
(timeline: (sequence: 14) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: checkpoint) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: automation) (target: 'https://example.com/next?token=[redacted]') (outcome: partial) (index: 2) (name: 'after click') (reason: checkpoint) (url: 'https://example.com/next?token=[redacted]') (truncated: true))
|
|
@@ -66,12 +66,13 @@ export const CHROME_LAUNCH_OPTIONS = {
|
|
|
66
66
|
* @returns {Promise<{browser: Object, page: Object, downloads: Object, cleanup: Function}>} The launched browser and a `cleanup` that closes it and removes the profile
|
|
67
67
|
*/
|
|
68
68
|
export async function launchE2EBrowser(options = {}) {
|
|
69
|
-
const { engine, downloadDirectory } = options;
|
|
69
|
+
const { engine, downloadDirectory, debug = false } = options;
|
|
70
70
|
|
|
71
71
|
// The default launch creates a fresh temporary profile and deletes it on
|
|
72
72
|
// close, so the helper no longer manages one itself.
|
|
73
73
|
const launched = await launchBrowser({
|
|
74
74
|
engine,
|
|
75
|
+
debug,
|
|
75
76
|
headless: process.env.HEADLESS !== 'false',
|
|
76
77
|
...CHROME_LAUNCH_OPTIONS,
|
|
77
78
|
...(downloadDirectory
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { startTrace } from '../../../src/traces/recorder.js';
|
|
7
|
+
import { readTrace } from '../../../src/traces/reader.js';
|
|
8
|
+
import { attachCommanderFeatures } from '../../../src/core/commander-features.js';
|
|
9
|
+
import { launchBrowserWithDependencies } from '../../../src/browser/launcher.js';
|
|
10
|
+
import { makeBrowserCommander } from '../../../src/factory.js';
|
|
11
|
+
import { attachLaunchDebug } from '../../../src/traces/debug-session.js';
|
|
12
|
+
import {
|
|
13
|
+
createFakePage,
|
|
14
|
+
createFakeCommander,
|
|
15
|
+
} from '../../helpers/trace-fixtures.js';
|
|
16
|
+
|
|
17
|
+
describe('complete recording defaults (issue #162)', { timeout: 15000 }, () => {
|
|
18
|
+
async function fixture(run) {
|
|
19
|
+
const output = await fs.mkdtemp(path.join(os.tmpdir(), 'bc-defaults-'));
|
|
20
|
+
const page = createFakePage();
|
|
21
|
+
const commander = createFakeCommander(page, { destroy: async () => {} });
|
|
22
|
+
const previous = { ...process.env };
|
|
23
|
+
delete process.env.BROWSER_COMMANDER_DEBUG;
|
|
24
|
+
delete process.env.BROWSER_COMMANDER_TRACE;
|
|
25
|
+
try {
|
|
26
|
+
await run({ output, page, commander });
|
|
27
|
+
} finally {
|
|
28
|
+
await commander.destroy();
|
|
29
|
+
for (const key of [
|
|
30
|
+
'BROWSER_COMMANDER_DEBUG',
|
|
31
|
+
'BROWSER_COMMANDER_TRACE',
|
|
32
|
+
]) {
|
|
33
|
+
if (previous[key] === undefined) {
|
|
34
|
+
delete process.env[key];
|
|
35
|
+
} else {
|
|
36
|
+
process.env[key] = previous[key];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
await fs.rm(output, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
it('captures an initial snapshot, changes, actions and network with a default links file', async () => {
|
|
44
|
+
await fixture(async ({ output, page, commander }) => {
|
|
45
|
+
const trace = await startTrace({ commander, output, screenshots: false });
|
|
46
|
+
try {
|
|
47
|
+
assert.equal(trace.mode, 'continuous');
|
|
48
|
+
assert.equal(trace.checkpoints[0].name, 'initial');
|
|
49
|
+
assert.equal(trace.links, path.join(output, 'trace.lino'));
|
|
50
|
+
assert.ok(page.listenerCount('request') > 0);
|
|
51
|
+
assert.ok(
|
|
52
|
+
page.state.initScripts.some(
|
|
53
|
+
(call) => call.name === 'installMutationRecorderInPage'
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
await commander.click('#delete');
|
|
57
|
+
} finally {
|
|
58
|
+
await trace.stop();
|
|
59
|
+
}
|
|
60
|
+
const opened = await readTrace(output);
|
|
61
|
+
assert.equal(
|
|
62
|
+
opened.events.find((event) => event.kind === 'interaction').source,
|
|
63
|
+
'commander'
|
|
64
|
+
);
|
|
65
|
+
assert.match(
|
|
66
|
+
await fs.readFile(trace.links, 'utf8'),
|
|
67
|
+
/initial|dom-snapshot/
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('preserves explicit checkpoint, network and links opt-outs', async () => {
|
|
73
|
+
await fixture(async ({ output, page, commander }) => {
|
|
74
|
+
const trace = await startTrace({
|
|
75
|
+
commander,
|
|
76
|
+
output,
|
|
77
|
+
mode: 'checkpoints',
|
|
78
|
+
network: false,
|
|
79
|
+
links: false,
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
assert.equal(trace.mode, 'checkpoints');
|
|
83
|
+
assert.equal(trace.checkpoints.length, 0);
|
|
84
|
+
assert.equal(trace.links, null);
|
|
85
|
+
assert.equal(page.listenerCount('request'), 0);
|
|
86
|
+
} finally {
|
|
87
|
+
await trace.stop();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('rejects unbounded action queues before opening recording files', async () => {
|
|
93
|
+
await fixture(async ({ output, page }) => {
|
|
94
|
+
for (const maxQueuedUserActions of [Infinity, 0, -1, 1.5]) {
|
|
95
|
+
await assert.rejects(
|
|
96
|
+
startTrace({ page, output, limits: { maxQueuedUserActions } }),
|
|
97
|
+
/maxQueuedUserActions/
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
assert.deepEqual(await fs.readdir(output), []);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('starts commander debugging immediately, and finalizes it on destroy', async () => {
|
|
105
|
+
await fixture(async ({ output, commander }) => {
|
|
106
|
+
attachCommanderFeatures(commander, { debug: { output } }, () => {});
|
|
107
|
+
assert.ok(commander.ready instanceof Promise);
|
|
108
|
+
await commander.ready;
|
|
109
|
+
assert.equal(commander.tracePath, output);
|
|
110
|
+
await commander.destroy();
|
|
111
|
+
assert.equal((await readTrace(output)).manifest.mode, 'continuous');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('records the very first call with no options and chooses a unique session directory', async () => {
|
|
116
|
+
await fixture(async ({ output, page }) => {
|
|
117
|
+
const cwd = process.cwd();
|
|
118
|
+
process.chdir(output);
|
|
119
|
+
page.locator = () => ({});
|
|
120
|
+
page.context = () => ({});
|
|
121
|
+
page.url = () => 'https://example.com/first';
|
|
122
|
+
let commander;
|
|
123
|
+
try {
|
|
124
|
+
commander = makeBrowserCommander({
|
|
125
|
+
page,
|
|
126
|
+
enableNetworkTracking: false,
|
|
127
|
+
enableNavigationManager: false,
|
|
128
|
+
enableDialogManager: false,
|
|
129
|
+
});
|
|
130
|
+
page.goto = async () => true;
|
|
131
|
+
await commander.goto({
|
|
132
|
+
url: 'https://example.com/first',
|
|
133
|
+
waitAfter: false,
|
|
134
|
+
});
|
|
135
|
+
assert.match(
|
|
136
|
+
commander.tracePath,
|
|
137
|
+
/browser-commander-traces[/\\].+-\d+-[a-f0-9]{8}$/
|
|
138
|
+
);
|
|
139
|
+
await commander.destroy();
|
|
140
|
+
const trace = await readTrace(commander.tracePath);
|
|
141
|
+
assert.equal(
|
|
142
|
+
trace.events.filter((event) => event.source === 'commander').length,
|
|
143
|
+
1
|
|
144
|
+
);
|
|
145
|
+
} finally {
|
|
146
|
+
await commander?.destroy();
|
|
147
|
+
process.chdir(cwd);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
for (const disabled of ['option', 'environment']) {
|
|
153
|
+
it(`respects the ${disabled} opt-out even with BROWSER_COMMANDER_TRACE`, async () => {
|
|
154
|
+
await fixture(async ({ output, page, commander }) => {
|
|
155
|
+
process.env.BROWSER_COMMANDER_TRACE = output;
|
|
156
|
+
if (disabled === 'environment') {
|
|
157
|
+
process.env.BROWSER_COMMANDER_DEBUG = '0';
|
|
158
|
+
}
|
|
159
|
+
attachCommanderFeatures(
|
|
160
|
+
commander,
|
|
161
|
+
disabled === 'option' ? { debug: false } : {},
|
|
162
|
+
() => {}
|
|
163
|
+
);
|
|
164
|
+
await commander.ready;
|
|
165
|
+
assert.equal(page.listenerCount('request'), 0);
|
|
166
|
+
assert.equal(commander.tracePath, null);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
it('keeps a launch opt-out when a default commander adopts the page', async () => {
|
|
172
|
+
await fixture(async ({ output, page, commander }) => {
|
|
173
|
+
const launched = await attachLaunchDebug({ page }, { debug: false });
|
|
174
|
+
attachCommanderFeatures(commander, { output }, () => {});
|
|
175
|
+
await commander.ready;
|
|
176
|
+
assert.equal(launched.tracePath, null);
|
|
177
|
+
assert.equal(commander.tracePath, null);
|
|
178
|
+
assert.equal(page.listenerCount('request'), 0);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('starts a launch recording before returning, reuses it in a commander, and closes it', async () => {
|
|
183
|
+
await fixture(async ({ output, page, commander }) => {
|
|
184
|
+
let closed = 0;
|
|
185
|
+
page.bringToFront = async () => {};
|
|
186
|
+
const browser = {
|
|
187
|
+
pages: () => [page],
|
|
188
|
+
close: async () => {
|
|
189
|
+
closed += 1;
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
const launched = await launchBrowserWithDependencies(
|
|
193
|
+
{ debug: { output } },
|
|
194
|
+
{
|
|
195
|
+
resolveSystem: async () => '/usr/bin/google-chrome',
|
|
196
|
+
settleMs: 0,
|
|
197
|
+
launchRealBrowser: async () => ({
|
|
198
|
+
page,
|
|
199
|
+
browser: { contexts: () => [browser] },
|
|
200
|
+
close: browser.close,
|
|
201
|
+
}),
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
try {
|
|
205
|
+
assert.equal(launched.tracePath, output);
|
|
206
|
+
const requests = page.listenerCount('request');
|
|
207
|
+
attachCommanderFeatures(commander, {}, () => {});
|
|
208
|
+
await commander.ready;
|
|
209
|
+
assert.equal(commander.tracePath, output);
|
|
210
|
+
assert.equal(page.listenerCount('request'), requests);
|
|
211
|
+
await commander.click('#delete');
|
|
212
|
+
} finally {
|
|
213
|
+
await launched.close();
|
|
214
|
+
}
|
|
215
|
+
assert.equal(closed, 1);
|
|
216
|
+
const opened = await readTrace(output);
|
|
217
|
+
assert.equal(
|
|
218
|
+
opened.events.filter((event) => event.kind === 'interaction').length,
|
|
219
|
+
1
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|