browser-commander 0.21.0 → 0.21.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.
Files changed (53) hide show
  1. package/.jscpd.json +2 -1
  2. package/.prettierignore +1 -0
  3. package/CHANGELOG.md +14 -0
  4. package/package.json +4 -2
  5. package/src/browser/attach/snapshot.js +4 -4
  6. package/src/browser/browser-cookie-cache.js +14 -2
  7. package/src/browser/profile-directory.js +109 -6
  8. package/src/browser/real-browser.js +38 -6
  9. package/src/cli/args.js +47 -0
  10. package/src/cli/sessions.js +4 -0
  11. package/src/traces/bundle.js +3 -2
  12. package/src/traces/observers.js +14 -5
  13. package/src/traces/recorder.js +3 -0
  14. package/src/traces/viewer.js +16 -8
  15. package/tests/e2e/playwright.e2e.test.js +1 -0
  16. package/tests/e2e/puppeteer.e2e.test.js +1 -0
  17. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.html +2 -0
  18. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.png +1 -0
  19. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.state.json +54 -0
  20. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +2 -0
  21. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.png +1 -0
  22. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.state.json +60 -0
  23. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +2 -0
  24. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.png +1 -0
  25. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.state.json +60 -0
  26. package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +18 -0
  27. package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +77 -0
  28. package/tests/fixtures/traces/conformance/expected/bundle/mutations/0001.ndjson +2 -0
  29. package/tests/fixtures/traces/conformance/expected/bundle/mutations/0002.ndjson +1 -0
  30. package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +309 -0
  31. package/tests/fixtures/traces/conformance/expected/redaction.json +86 -0
  32. package/tests/fixtures/traces/conformance/expected/trace.lino +27 -0
  33. package/tests/fixtures/traces/conformance/scenario.json +249 -0
  34. package/tests/unit/browser/attach/extension-relay.test.js +1 -1
  35. package/tests/unit/browser/attach/snapshot-launch.test.js +10 -1
  36. package/tests/unit/browser/attach/snapshot.test.js +23 -4
  37. package/tests/unit/browser/browser-cookie-lock.test.js +102 -0
  38. package/tests/unit/browser/launch-building-blocks.test.js +127 -4
  39. package/tests/unit/browser/migration/index.test.js +1 -1
  40. package/tests/unit/browser/open-in-user-browser.test.js +1 -1
  41. package/tests/unit/browser/parity.test.js +1 -1
  42. package/tests/unit/browser/real-browser.test.js +1 -1
  43. package/tests/unit/browser/storage-state.test.js +2 -0
  44. package/tests/unit/cli/main.test.js +30 -2
  45. package/tests/unit/core/navigation-manager.test.js +11 -1
  46. package/tests/unit/playwright-protocol-coverage.test.js +231 -0
  47. package/tests/unit/puppeteer-bindings-coverage.test.js +214 -0
  48. package/tests/unit/scripts/ci-timeout-budgets.test.js +23 -0
  49. package/tests/unit/scripts/feature-parity.test.js +111 -5
  50. package/tests/unit/traces/conformance.test.js +92 -0
  51. package/tests/unit/traces/reader.test.js +1 -1
  52. package/tests/unit/traces/recorder.test.js +1 -1
  53. package/tests/unit/traces/viewer.test.js +78 -0
@@ -0,0 +1,309 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <!-- Inert by default: no network, no captured scripts, images only from data URLs. -->
6
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; frame-src 'self' data:;">
7
+ <title>Browser Commander trace</title>
8
+ <style>
9
+ :root { color-scheme: light dark; font-family: system-ui, sans-serif; }
10
+ body { margin: 0; display: grid; grid-template-columns: 22rem 1fr;
11
+ grid-template-rows: auto 1fr; height: 100vh; }
12
+ header { grid-column: 1 / -1; padding: 0.5rem 1rem; border-bottom: 1px solid #8884; }
13
+ h1 { font-size: 1rem; margin: 0 0 0.25rem; }
14
+ .meta { font-size: 0.75rem; opacity: 0.75; }
15
+ .meta.replay { opacity: 0.9; font-style: italic; }
16
+ aside { overflow: auto; border-right: 1px solid #8884; }
17
+ main { display: grid; grid-template-rows: auto 1fr auto; overflow: hidden; }
18
+ ol { list-style: none; margin: 0; padding: 0; font-size: 0.8rem; }
19
+ li { padding: 0.35rem 0.75rem; border-bottom: 1px solid #8882; cursor: pointer; }
20
+ li.selected { background: #3b82f633; }
21
+ li .kind { font-weight: 600; }
22
+ li .at { opacity: 0.6; float: right; }
23
+ iframe { width: 100%; height: 100%; border: 0; background: #fff; }
24
+ .panel { padding: 0.5rem 1rem; border-top: 1px solid #8884; max-height: 14rem;
25
+ overflow: auto; font-size: 0.8rem; }
26
+ table { border-collapse: collapse; width: 100%; }
27
+ td, th { text-align: left; padding: 0.15rem 0.5rem; border-bottom: 1px solid #8882;
28
+ font-family: ui-monospace, monospace; font-size: 0.75rem; }
29
+ .controls { padding: 0.5rem 1rem; display: flex; gap: 0.5rem; align-items: center; }
30
+ .dropped { color: #b91c1c; }
31
+ button { font: inherit; }
32
+ </style>
33
+ </head>
34
+ <body>
35
+ <header>
36
+ <h1>Browser Commander trace</h1>
37
+ <div class="meta">
38
+ mode continuous · outcome partial · engine playwright ·
39
+ 3 checkpoints · 18 events ·
40
+ 1 dropped · started 2026-01-01T00:00:00.000Z
41
+ </div>
42
+ <div class="meta replay">partial diagnostic replay: checkpoints, DOM mutations, insertion positions and removals, live control state, page and frame identities</div>
43
+ </header>
44
+ <aside><ol id="timeline"></ol></aside>
45
+ <main>
46
+ <div class="controls">
47
+ <button id="play" type="button">Replay mutations</button>
48
+ <button id="step-forward" type="button">Step</button>
49
+ <button id="reset" type="button">Reset</button>
50
+ <span id="step"></span>
51
+ </div>
52
+ <iframe id="stage" sandbox referrerpolicy="no-referrer" title="captured page"></iframe>
53
+ <div class="panel" id="details"></div>
54
+ <div class="panel" id="diff"></div>
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]","[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","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},{"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},{"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\u003esk-zzz9 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\u003esk-zzz9 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
+ <script>
58
+ const trace = JSON.parse(document.getElementById('trace-data').textContent);
59
+ const timeline = document.getElementById('timeline');
60
+ const frame = document.getElementById('stage');
61
+ const details = document.getElementById('details');
62
+ const diffPanel = document.getElementById('diff');
63
+ const stepLabel = document.getElementById('step');
64
+ let selectedCheckpoint = null;
65
+ let mutationStep = 0;
66
+
67
+ // Everything in the trace is what the recorded page made it, and the policy
68
+ // allows inline handlers for this script, so recorded text is escaped before
69
+ // it is put into the viewer's markup.
70
+ function escapeHtml(value) {
71
+ return String(value).replace(/[&<>"']/g, (c) => '&#' + c.charCodeAt(0) + ';');
72
+ }
73
+
74
+ function checkpointFor(event) {
75
+ if (event.kind === 'checkpoint') return event.index;
76
+ let latest = null;
77
+ for (const other of trace.events) {
78
+ if (other.sequence > event.sequence) break;
79
+ if (other.kind === 'checkpoint') latest = other.index;
80
+ }
81
+ return latest;
82
+ }
83
+
84
+ function renderHtml(index, html) {
85
+ // The captured document is handed to a sandboxed frame with no
86
+ // allow-scripts and no allow-forms: it is shown, never run.
87
+ frame.srcdoc = html == null ? '<p style="font:1rem system-ui">no HTML captured for this checkpoint</p>' : html;
88
+ }
89
+
90
+ function nodeOf(parsed, described) {
91
+ if (!described) return null;
92
+ if (described.type === 'text') {
93
+ return parsed.createTextNode(described.text == null ? '' : described.text);
94
+ }
95
+ if (described.type === 'element' && described.html) {
96
+ const holder = parsed.createElement('template');
97
+ holder.innerHTML = described.html;
98
+ return holder.content.firstChild;
99
+ }
100
+ return null;
101
+ }
102
+
103
+ function looksLike(node, described) {
104
+ if (!node || !described) return false;
105
+ if (described.type === 'text') return node.nodeType === 3;
106
+ if (described.type !== 'element') return false;
107
+ if (node.nodeType !== 1 || node.localName !== described.tag) return false;
108
+ return described.id ? node.id === described.id : true;
109
+ }
110
+
111
+ function childLike(parent, described) {
112
+ if (!described) return null;
113
+ // An id is identity; an index is only a hint. A node that moved is reported
114
+ // at the position it moved *to*, so the index alone would find the wrong one.
115
+ if (described.id) {
116
+ for (const child of parent.childNodes) {
117
+ if (child.nodeType === 1 && child.id === described.id) return child;
118
+ }
119
+ }
120
+ const at = described.index == null ? null : parent.childNodes[described.index];
121
+ if (looksLike(at, described)) return at;
122
+ for (const child of parent.childNodes) {
123
+ if (looksLike(child, described)) return child;
124
+ }
125
+ return null;
126
+ }
127
+
128
+ function insertAt(parent, node, index) {
129
+ if (!node) return false;
130
+ const reference = index == null ? null : parent.childNodes[index] || null;
131
+ parent.insertBefore(node, reference);
132
+ return true;
133
+ }
134
+
135
+ function applyLiveState(parsed, target, record) {
136
+ const value = record.after;
137
+ const text = value == null ? '' : String(value);
138
+ if (record.property === 'scroll') {
139
+ // A static copy of a document cannot be scrolled, so where it was
140
+ // scrolled to is written onto the element rather than quietly lost.
141
+ const element = target || parsed.documentElement;
142
+ const at = value || {};
143
+ element.setAttribute('data-bc-scroll', at.top + ',' + at.left);
144
+ return true;
145
+ }
146
+ if (!target) return false;
147
+ if (record.property === 'value') {
148
+ // Serialized HTML carries attributes, not properties, so what was typed
149
+ // is written where a re-parse will still find it.
150
+ if (target.localName === 'textarea' || target.hasAttribute('contenteditable')) {
151
+ target.textContent = text;
152
+ } else {
153
+ target.setAttribute('value', text);
154
+ }
155
+ return true;
156
+ }
157
+ if (record.property === 'checked') {
158
+ if (value) target.setAttribute('checked', '');
159
+ else target.removeAttribute('checked');
160
+ return true;
161
+ }
162
+ if (record.property === 'selected') {
163
+ const chosen = (Array.isArray(value) ? value : []).map(String);
164
+ for (const option of target.querySelectorAll('option')) {
165
+ if (chosen.indexOf(option.value) !== -1) option.setAttribute('selected', '');
166
+ else option.removeAttribute('selected');
167
+ }
168
+ return true;
169
+ }
170
+ if (record.property === 'focus') {
171
+ for (const had of parsed.querySelectorAll('[data-bc-focus]')) {
172
+ had.removeAttribute('data-bc-focus');
173
+ }
174
+ if (value) target.setAttribute('data-bc-focus', '');
175
+ return true;
176
+ }
177
+ return false;
178
+ }
179
+
180
+ function applyMutations(index, upTo) {
181
+ const html = trace.html[index];
182
+ if (html == null) return null;
183
+ const parsed = new DOMParser().parseFromString(html, 'text/html');
184
+ const batches = trace.mutations[index] || [];
185
+ let applied = 0;
186
+ let skipped = 0;
187
+ for (const batch of batches.slice(0, upTo)) {
188
+ // The stage holds one document. A batch from an iframe belongs to a
189
+ // different one, so it is counted rather than applied to the wrong page.
190
+ if (batch.mainFrame === false) { skipped += (batch.records || []).length; continue; }
191
+ for (const record of batch.records) {
192
+ const target = record.target && record.target.path
193
+ ? parsed.querySelector(record.target.path)
194
+ : null;
195
+ if (record.kind === 'live-state') {
196
+ if (applyLiveState(parsed, target, record)) applied++; else skipped++;
197
+ } else if (!target) {
198
+ skipped++;
199
+ continue;
200
+ } else if (record.kind === 'attributes' && record.attribute) {
201
+ if (record.after == null) target.removeAttribute(record.attribute);
202
+ else target.setAttribute(record.attribute, record.after);
203
+ applied++;
204
+ } else if (record.kind === 'characterData') {
205
+ target.textContent = record.after == null ? '' : record.after;
206
+ applied++;
207
+ } else if (record.kind === 'childList') {
208
+ // Where a node went is as much of the change as what it was: appending
209
+ // every addition to the end replays an insertion before a sibling in
210
+ // the wrong place, and drops removals entirely (issue #93).
211
+ for (const removed of record.removed || []) {
212
+ const node = childLike(target, removed);
213
+ if (node) { target.removeChild(node); applied++; } else { skipped++; }
214
+ }
215
+ const added = (record.added || []).filter(Boolean)
216
+ .slice().sort((a, b) => (a.index == null ? 1e9 : a.index) - (b.index == null ? 1e9 : b.index));
217
+ for (const entry of added) {
218
+ if (insertAt(target, nodeOf(parsed, entry), entry.index)) applied++;
219
+ else skipped++;
220
+ }
221
+ } else {
222
+ skipped++;
223
+ }
224
+ if (target && target.style) target.style.outline = '2px solid #f59e0b';
225
+ }
226
+ }
227
+ return { html: '<!DOCTYPE html>' + parsed.documentElement.outerHTML, applied, skipped };
228
+ }
229
+
230
+ function stepText(step, total, result) {
231
+ const base = 'batch ' + step + ' of ' + total;
232
+ if (!result || !result.skipped) return base;
233
+ return base + ' · ' + result.skipped + ' not replayable here';
234
+ }
235
+
236
+ function showCheckpoint(index) {
237
+ selectedCheckpoint = index;
238
+ mutationStep = 0;
239
+ renderHtml(index, trace.html[index]);
240
+ const state = trace.state[index];
241
+ const previous = trace.state[index - 1];
242
+ details.innerHTML = state
243
+ ? '<table><tr><th>url</th><td>' + escapeHtml(state.url) + '</td></tr>' +
244
+ '<tr><th>title</th><td>' + escapeHtml(state.title || '') + '</td></tr>' +
245
+ '<tr><th>controls</th><td>' + (state.controls || []).length + '</td></tr></table>'
246
+ : '<p>no state captured</p>';
247
+ const changes = trace.diffs[index] || [];
248
+ diffPanel.innerHTML = previous && changes.length
249
+ ? '<table><tr><th>control</th><th>before</th><th>after</th></tr>' +
250
+ changes.map((c) => '<tr><td>' + escapeHtml(c.path) + '</td><td>' +
251
+ escapeHtml(c.before ?? '') + '</td><td>' + escapeHtml(c.after ?? '') +
252
+ '</td></tr>').join('') + '</table>'
253
+ : '<p>no control changes against the previous checkpoint</p>';
254
+ stepLabel.textContent = (trace.mutations[index] || []).length + ' mutation batches';
255
+ }
256
+
257
+ function select(event, element) {
258
+ for (const li of timeline.children) li.classList.remove('selected');
259
+ element.classList.add('selected');
260
+ const index = checkpointFor(event);
261
+ if (index != null && index !== selectedCheckpoint) showCheckpoint(index);
262
+ if (event.kind !== 'checkpoint') {
263
+ details.innerHTML = '<table>' + Object.entries(event)
264
+ .map(([k, v]) => '<tr><th>' + escapeHtml(k) + '</th><td>' + escapeHtml(String(
265
+ typeof v === 'object' ? JSON.stringify(v) : v).slice(0, 400)) + '</td></tr>')
266
+ .join('') + '</table>';
267
+ }
268
+ }
269
+
270
+ for (const event of trace.events) {
271
+ const li = document.createElement('li');
272
+ if (event.kind === 'dropped') li.className = 'dropped';
273
+ li.innerHTML = '<span class="kind"></span> <span class="at"></span><div class="what"></div>';
274
+ li.querySelector('.kind').textContent = event.kind;
275
+ li.querySelector('.at').textContent = (event.at || '').slice(11, 23);
276
+ li.querySelector('.what').textContent =
277
+ event.name || event.action || event.text || event.message || event.url || event.reason || '';
278
+ li.addEventListener('click', () => select(event, li));
279
+ timeline.append(li);
280
+ }
281
+
282
+ document.getElementById('play').addEventListener('click', async () => {
283
+ if (selectedCheckpoint == null) return;
284
+ const batches = trace.mutations[selectedCheckpoint] || [];
285
+ for (let step = 1; step <= batches.length; step++) {
286
+ const result = applyMutations(selectedCheckpoint, step);
287
+ if (result) frame.srcdoc = result.html;
288
+ stepLabel.textContent = stepText(step, batches.length, result);
289
+ await new Promise((resolve) => setTimeout(resolve, 250));
290
+ }
291
+ });
292
+
293
+ document.getElementById('step-forward').addEventListener('click', () => {
294
+ if (selectedCheckpoint == null) return;
295
+ const batches = trace.mutations[selectedCheckpoint] || [];
296
+ mutationStep = Math.min(mutationStep + 1, batches.length);
297
+ const result = applyMutations(selectedCheckpoint, mutationStep);
298
+ if (result) frame.srcdoc = result.html;
299
+ stepLabel.textContent = stepText(mutationStep, batches.length, result);
300
+ });
301
+
302
+ document.getElementById('reset').addEventListener('click', () => {
303
+ if (selectedCheckpoint != null) showCheckpoint(selectedCheckpoint);
304
+ });
305
+
306
+ if (trace.checkpoints.length) showCheckpoint(trace.checkpoints[0].index);
307
+ </script>
308
+ </body>
309
+ </html>
@@ -0,0 +1,86 @@
1
+ [
2
+ {
3
+ "url": "https://example.com/path?token=abc&keep=1",
4
+ "redacted": "https://example.com/path?token=[redacted]&keep=1"
5
+ },
6
+ {
7
+ "url": "https://EXAMPLE.com:443/a/../b/./c?Token=x#frag",
8
+ "redacted": "https://example.com/b/c?Token=[redacted]#frag"
9
+ },
10
+ {
11
+ "url": "http://example.com",
12
+ "redacted": "http://example.com/"
13
+ },
14
+ {
15
+ "url": "https://name:pw@example.com/?q=1",
16
+ "redacted": "https://[redacted]:[redacted]@example.com/?q=1"
17
+ },
18
+ {
19
+ "url": "https://user@example.com/x",
20
+ "redacted": "https://[redacted]@example.com/x"
21
+ },
22
+ {
23
+ "url": "https://example.com/?a=1&a=2&code=zz&b=hello%20world",
24
+ "redacted": "https://example.com/?a=1&a=2&code=[redacted]&b=hello+world"
25
+ },
26
+ {
27
+ "url": "https://example.com/?q=a b&secret=s",
28
+ "redacted": "https://example.com/?q=a+b&secret=[redacted]"
29
+ },
30
+ {
31
+ "url": "https://example.com/?q=a+b&secret=s",
32
+ "redacted": "https://example.com/?q=a+b&secret=[redacted]"
33
+ },
34
+ {
35
+ "url": "https://example.com/?q=a%2Bb&session=1&x=~!*()'",
36
+ "redacted": "https://example.com/?q=a%2Bb&session=[redacted]&x=%7E%21*%28%29%27"
37
+ },
38
+ {
39
+ "url": "https://example.com/cb#access_token=t&state=s",
40
+ "redacted": "https://example.com/cb#access_token=[redacted]&state=s"
41
+ },
42
+ {
43
+ "url": "https://example.com/cb#plain-fragment",
44
+ "redacted": "https://example.com/cb#plain-fragment"
45
+ },
46
+ {
47
+ "url": "https://example.com/p a th/ü?x=ü&password=p",
48
+ "redacted": "https://example.com/p%20a%20th/%C3%BC?x=%C3%BC&password=[redacted]"
49
+ },
50
+ {
51
+ "url": "https://example.com/?keep=1",
52
+ "redacted": "https://example.com/?keep=1"
53
+ },
54
+ {
55
+ "url": "https://example.com/?token",
56
+ "redacted": "https://example.com/?token=[redacted]"
57
+ },
58
+ {
59
+ "url": "https://example.com/?=token&token=",
60
+ "redacted": "https://example.com/?=token&token=[redacted]"
61
+ },
62
+ {
63
+ "url": "http://example.com:8080/?api_key=1",
64
+ "redacted": "http://example.com:8080/?api_key=[redacted]"
65
+ },
66
+ {
67
+ "url": "about:blank",
68
+ "redacted": "about:blank"
69
+ },
70
+ {
71
+ "url": "data:text/html,<p>hi</p>",
72
+ "redacted": "data:text/html,<p>hi</p>"
73
+ },
74
+ {
75
+ "url": "/relative/path?token=abc",
76
+ "redacted": "/relative/path?token=abc"
77
+ },
78
+ {
79
+ "url": "not a url",
80
+ "redacted": "not a url"
81
+ },
82
+ {
83
+ "url": "",
84
+ "redacted": ""
85
+ }
86
+ ]
@@ -0,0 +1,27 @@
1
+ (trace: (format: browser-commander-trace) (links: 1) (schema: 2) (bundle: bundle) (mode: continuous) (engine: playwright) (started: '2026-01-01T00:00:00.000Z') (commander: 0.0.0-conformance))
2
+ (timeline: (sequence: 1) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: trace.start) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-1) (actor: recorder) (outcome: recorded) (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))
3
+ (timeline: (sequence: 2) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: checkpoint) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-1) (actor: recorder) (target: 'https://[redacted]:[redacted]@example.com/start?ticket=[redacted]&lang=en#access_token=[redacted]&view=1') (outcome: recorded) (index: 1) (name: initial) (reason: initial) (url: 'https://[redacted]:[redacted]@example.com/start?ticket=[redacted]&lang=en#access_token=[redacted]&view=1') (truncated: false))
4
+ (checkpoint: (index: 1) (sequence: 2) (at: '2026-01-01T00:00:00.000Z') (name: initial) (actor: recorder) (reason: initial) (page: page-1) (navigation: nav-1) (url: 'https://[redacted]:[redacted]@example.com/start?ticket=[redacted]&lang=en#access_token=[redacted]&view=1') (outcome: recorded) (html: checkpoints/0001.html) (state: checkpoints/0001.state.json) (screenshot: checkpoints/0001.png))
5
+ (timeline: (sequence: 3) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: navigation) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: browser) (action: framenavigated) (target: 'https://example.com/next?token=[redacted]&q=a+b') (outcome: recorded) (phase: framenavigated) (mainFrame: true) (url: 'https://example.com/next?token=[redacted]&q=a+b'))
6
+ (timeline: (sequence: 4) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: navigation) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: browser) (action: framenavigated) (target: 'https://ads.example.com/frame') (outcome: recorded) (phase: framenavigated) (mainFrame: false) (url: 'https://ads.example.com/frame'))
7
+ (timeline: (sequence: 5) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: console) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: browser) (outcome: recorded) (level: log) (text: "key [redacted] loaded\n\ttab \u0022and\u0022 'quotes'"))
8
+ (timeline: (sequence: 6) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: pageerror) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: browser) (outcome: recorded) (message: 'boom: (unbalanced') (stack: 'Error: boom\n at main.js:1:2'))
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
+ (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
+ (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'))
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
+ (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))
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))
17
+ (checkpoint: (index: 2) (sequence: 14) (at: '2026-01-01T00:00:00.000Z') (name: 'after click') (actor: automation) (reason: checkpoint) (page: page-1) (navigation: nav-2) (url: 'https://example.com/next?token=[redacted]') (outcome: partial) (html: checkpoints/0002.html) (state: checkpoints/0002.state.json) (screenshot: checkpoints/0002.png))
18
+ (timeline: (sequence: 15) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: interaction) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: caller) (action: note) (outcome: recorded) (authorization: [redacted]) (detail: 'uses [redacted]') (pageUrl: 'https://example.com/?session=[redacted]') (count: 3) (ratio: 0.25) (whole: 2) (nested: '{"list":[1,"two",null,true]}'))
19
+ (timeline: (sequence: 16) (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/0002.ndjson) (outcome: recorded) (member: mutations/0002.ndjson) (batches: 1) (checkpoint: 2) (frames: 1))
20
+ (timeline: (sequence: 17) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: checkpoint) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: caller) (target: 'https://example.com/next?token=[redacted]') (outcome: partial) (index: 3) (name: final) (reason: failure) (url: 'https://example.com/next?token=[redacted]') (truncated: true))
21
+ (checkpoint: (index: 3) (sequence: 17) (at: '2026-01-01T00:00:00.000Z') (name: final) (actor: caller) (reason: failure) (page: page-1) (navigation: nav-2) (url: 'https://example.com/next?token=[redacted]') (outcome: partial) (html: checkpoints/0003.html) (state: checkpoints/0003.state.json) (screenshot: checkpoints/0003.png))
22
+ (timeline: (sequence: 18) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: trace.stop) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: recorder) (outcome: recorded) (discarded: false))
23
+ (control-diff: (checkpoint: 2) (previous: 1) (path: 'form > input:nth-of-type(1)') (change: changed) (before: alice) (after: bob) (actor: automation))
24
+ (control-diff: (checkpoint: 2) (previous: 1) (path: 'form > input:nth-of-type(2)') (change: changed) (before: false) (after: true) (actor: automation))
25
+ (control-diff: (checkpoint: 2) (previous: 1) (path: 'form > textarea') (change: added) (after: '[redacted] note') (actor: automation))
26
+ (control-diff: (checkpoint: 2) (previous: 1) (path: 'form > select') (change: removed) (before: a) (actor: automation))
27
+ (result: (outcome: partial) (stopped: '2026-01-01T00:00:00.000Z') (events: 18) (checkpoints: 3) (mutationBatches: 3) (dropped: 1) (truncated: true) (replay: checkpoints mutations childListPositions liveState identifiers))