browser-commander 0.20.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 (169) hide show
  1. package/.jscpd.json +2 -1
  2. package/.prettierignore +1 -0
  3. package/CHANGELOG.md +159 -0
  4. package/README.md +216 -32
  5. package/bin/browser-commander.js +16 -0
  6. package/eslint.config.js +10 -0
  7. package/examples/react-test-app/src/App.jsx +24 -23
  8. package/extension/background.js +14 -0
  9. package/extension/manifest.json +12 -0
  10. package/extension/relay-handler.js +353 -0
  11. package/package.json +21 -4
  12. package/src/browser/attach/differences.js +131 -0
  13. package/src/browser/attach/extension-relay.js +402 -0
  14. package/src/browser/attach/index.js +81 -0
  15. package/src/browser/attach/snapshot-launch.js +146 -0
  16. package/src/browser/attach/snapshot.js +374 -0
  17. package/src/browser/attach/websocket.js +615 -0
  18. package/src/browser/browser-cookie-cache.js +26 -19
  19. package/src/browser/browser-cookie-credentials.js +6 -9
  20. package/src/browser/browser-cookies.js +5 -2
  21. package/src/browser/cdp-session.js +94 -0
  22. package/src/browser/connector.js +42 -4
  23. package/src/browser/debugging-port.js +155 -0
  24. package/src/browser/launch-options.js +63 -52
  25. package/src/browser/launch-restrictions.json +147 -0
  26. package/src/browser/launcher.js +329 -133
  27. package/src/browser/media.js +5 -0
  28. package/src/browser/migration/bookmarks.js +73 -0
  29. package/src/browser/migration/chromium-crypto.js +89 -0
  30. package/src/browser/migration/cookies.js +155 -0
  31. package/src/browser/migration/extensions.js +187 -0
  32. package/src/browser/migration/firefox-bookmarks.js +144 -0
  33. package/src/browser/migration/firefox-der.js +84 -0
  34. package/src/browser/migration/firefox-nss.js +281 -0
  35. package/src/browser/migration/firefox.js +334 -0
  36. package/src/browser/migration/fs-utils.js +51 -0
  37. package/src/browser/migration/history.js +103 -0
  38. package/src/browser/migration/index.js +307 -0
  39. package/src/browser/migration/os-crypt-keys.js +106 -0
  40. package/src/browser/migration/passwords.js +171 -0
  41. package/src/browser/migration/preferences.js +132 -0
  42. package/src/browser/migration/sqlite-snapshot.js +138 -0
  43. package/src/browser/open-in-user-browser.js +112 -0
  44. package/src/browser/parity.js +439 -0
  45. package/src/browser/profile-directory.js +205 -0
  46. package/src/browser/real-browser.js +545 -306
  47. package/src/browser/restrictions.js +148 -0
  48. package/src/browser/system-browser.js +232 -0
  49. package/src/browser/webdriver.js +572 -0
  50. package/src/cli/args.js +300 -0
  51. package/src/cli/commands.js +332 -0
  52. package/src/cli/dispatcher.js +99 -0
  53. package/src/cli/handle-methods.js +136 -0
  54. package/src/cli/handles.js +273 -0
  55. package/src/cli/high-level-methods.js +310 -0
  56. package/src/cli/main.js +67 -0
  57. package/src/cli/modules.js +100 -0
  58. package/src/cli/rpc-error.js +77 -0
  59. package/src/cli/script.js +77 -0
  60. package/src/cli/serve.js +144 -0
  61. package/src/cli/sessions.js +199 -0
  62. package/src/core/constants.js +7 -1
  63. package/src/core/engine-adapter.js +107 -1
  64. package/src/core/engine-detection.js +18 -3
  65. package/src/core/navigation-safety.js +4 -0
  66. package/src/core/webdriver-conversions.js +347 -0
  67. package/src/core/webdriver-events.js +229 -0
  68. package/src/core/webdriver-page.js +716 -0
  69. package/src/downloads/attach.js +6 -0
  70. package/src/elements/selectors.js +3 -2
  71. package/src/elements/visibility.js +4 -0
  72. package/src/exports.js +61 -2
  73. package/src/factory.js +19 -4
  74. package/src/fingerprint/apply.js +6 -0
  75. package/src/fingerprint/automation-parity.js +7 -8
  76. package/src/fingerprint/limitations.js +1 -1
  77. package/src/fingerprint/limitations.json +45 -2
  78. package/src/parity/harness.js +303 -0
  79. package/src/parity/probe.js +838 -0
  80. package/src/parity/version-page.js +144 -0
  81. package/src/traces/bundle.js +3 -2
  82. package/src/traces/observers.js +14 -5
  83. package/src/traces/recorder.js +3 -0
  84. package/src/traces/viewer.js +16 -8
  85. package/src/utilities/subprocess.js +199 -0
  86. package/tests/e2e/api-coverage.e2e.test.js +174 -0
  87. package/tests/e2e/cli.e2e.test.js +160 -0
  88. package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
  89. package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
  90. package/tests/e2e/measure-parity.e2e.test.js +118 -0
  91. package/tests/e2e/playwright.e2e.test.js +26 -17
  92. package/tests/e2e/puppeteer.e2e.test.js +18 -23
  93. package/tests/e2e/webdriver.e2e.test.js +239 -0
  94. package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
  95. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.html +2 -0
  96. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.png +1 -0
  97. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.state.json +54 -0
  98. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +2 -0
  99. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.png +1 -0
  100. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.state.json +60 -0
  101. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +2 -0
  102. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.png +1 -0
  103. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.state.json +60 -0
  104. package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +18 -0
  105. package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +77 -0
  106. package/tests/fixtures/traces/conformance/expected/bundle/mutations/0001.ndjson +2 -0
  107. package/tests/fixtures/traces/conformance/expected/bundle/mutations/0002.ndjson +1 -0
  108. package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +309 -0
  109. package/tests/fixtures/traces/conformance/expected/redaction.json +86 -0
  110. package/tests/fixtures/traces/conformance/expected/trace.lino +27 -0
  111. package/tests/fixtures/traces/conformance/scenario.json +249 -0
  112. package/tests/helpers/cli-fakes.js +214 -0
  113. package/tests/helpers/declared-api.js +80 -0
  114. package/tests/helpers/e2e-browser.js +40 -14
  115. package/tests/helpers/fake-chrome.js +124 -0
  116. package/tests/helpers/migration-fixtures.js +242 -0
  117. package/tests/helpers/temp-directory.js +24 -0
  118. package/tests/helpers/webdriver-mocks.js +183 -0
  119. package/tests/unit/browser/attach/extension-relay.test.js +360 -0
  120. package/tests/unit/browser/attach/snapshot-launch.test.js +321 -0
  121. package/tests/unit/browser/attach/snapshot.test.js +409 -0
  122. package/tests/unit/browser/attach/websocket.test.js +240 -0
  123. package/tests/unit/browser/browser-cookie-lock.test.js +102 -0
  124. package/tests/unit/browser/browser-cookies.test.js +2 -32
  125. package/tests/unit/browser/cdp-session.test.js +162 -0
  126. package/tests/unit/browser/connector.test.js +23 -0
  127. package/tests/unit/browser/launch-building-blocks.test.js +361 -0
  128. package/tests/unit/browser/launch-options.test.js +41 -22
  129. package/tests/unit/browser/launcher.test.js +281 -102
  130. package/tests/unit/browser/media.test.js +13 -1
  131. package/tests/unit/browser/migration/bookmarks.test.js +69 -0
  132. package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
  133. package/tests/unit/browser/migration/cookies.test.js +94 -0
  134. package/tests/unit/browser/migration/extensions.test.js +104 -0
  135. package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
  136. package/tests/unit/browser/migration/firefox.test.js +139 -0
  137. package/tests/unit/browser/migration/history.test.js +48 -0
  138. package/tests/unit/browser/migration/index.test.js +117 -0
  139. package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
  140. package/tests/unit/browser/migration/passwords.test.js +144 -0
  141. package/tests/unit/browser/migration/preferences.test.js +77 -0
  142. package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
  143. package/tests/unit/browser/open-in-user-browser.test.js +123 -0
  144. package/tests/unit/browser/parity.test.js +281 -0
  145. package/tests/unit/browser/real-browser.test.js +458 -33
  146. package/tests/unit/browser/storage-state.test.js +2 -0
  147. package/tests/unit/browser/webdriver.test.js +473 -0
  148. package/tests/unit/cli/bin.test.js +69 -0
  149. package/tests/unit/cli/declared-api.test.js +49 -0
  150. package/tests/unit/cli/dispatcher.test.js +429 -0
  151. package/tests/unit/cli/handles.test.js +170 -0
  152. package/tests/unit/cli/main.test.js +437 -0
  153. package/tests/unit/cli/serve.test.js +167 -0
  154. package/tests/unit/core/navigation-manager.test.js +11 -1
  155. package/tests/unit/core/webdriver-conversions.test.js +235 -0
  156. package/tests/unit/core/webdriver-page.test.js +534 -0
  157. package/tests/unit/fingerprint/automation-parity.test.js +17 -4
  158. package/tests/unit/fingerprint/limitations.test.js +1 -1
  159. package/tests/unit/parity/harness.test.js +138 -0
  160. package/tests/unit/playwright-protocol-coverage.test.js +231 -0
  161. package/tests/unit/puppeteer-bindings-coverage.test.js +214 -0
  162. package/tests/unit/scripts/ci-timeout-budgets.test.js +27 -0
  163. package/tests/unit/scripts/feature-parity.test.js +327 -0
  164. package/tests/unit/traces/conformance.test.js +92 -0
  165. package/tests/unit/traces/reader.test.js +1 -0
  166. package/tests/unit/traces/recorder.test.js +1 -0
  167. package/tests/unit/traces/viewer.test.js +78 -0
  168. package/tests/unit/utilities/command-stream-only.test.js +28 -0
  169. package/tests/unit/utilities/subprocess.test.js +80 -0
@@ -0,0 +1,60 @@
1
+ {
2
+ "url": "https://example.com/next?token=[redacted]",
3
+ "title": "Next",
4
+ "readyState": "interactive",
5
+ "scroll": {
6
+ "x": 0,
7
+ "y": 0
8
+ },
9
+ "viewport": {
10
+ "width": 1280,
11
+ "height": 720
12
+ },
13
+ "activeElement": "form > textarea",
14
+ "controls": [
15
+ {
16
+ "path": "form > input:nth-of-type(1)",
17
+ "tag": "input",
18
+ "type": "text",
19
+ "name": "user",
20
+ "id": null,
21
+ "focused": false,
22
+ "value": "bob",
23
+ "redacted": false
24
+ },
25
+ {
26
+ "path": "form > input:nth-of-type(2)",
27
+ "tag": "input",
28
+ "type": "checkbox",
29
+ "name": "agree",
30
+ "id": null,
31
+ "focused": false,
32
+ "value": "on",
33
+ "checked": true,
34
+ "redacted": false
35
+ },
36
+ {
37
+ "path": "form > textarea",
38
+ "tag": "textarea",
39
+ "type": null,
40
+ "name": null,
41
+ "id": "note",
42
+ "focused": true,
43
+ "value": "[redacted] note",
44
+ "scroll": {
45
+ "x": 0,
46
+ "y": 3
47
+ },
48
+ "redacted": false
49
+ }
50
+ ],
51
+ "frames": [
52
+ {
53
+ "url": "https://ads.example.com/frame",
54
+ "crossOrigin": true
55
+ }
56
+ ],
57
+ "name": "final",
58
+ "actor": "caller",
59
+ "reason": "failure"
60
+ }
@@ -0,0 +1,18 @@
1
+ {"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}
2
+ {"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"}}
3
+ {"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"}
4
+ {"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"}
5
+ {"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'"}
6
+ {"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"}
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
+ {"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
+ {"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"}
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
+ {"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}
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"}}
15
+ {"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]}}
16
+ {"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}
17
+ {"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"}}
18
+ {"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}
@@ -0,0 +1,77 @@
1
+ {
2
+ "schemaVersion": 2,
3
+ "format": "browser-commander-trace",
4
+ "mode": "continuous",
5
+ "outcome": "partial",
6
+ "startedAt": "2026-01-01T00:00:00.000Z",
7
+ "stoppedAt": "2026-01-01T00:00:00.000Z",
8
+ "commanderVersion": "0.0.0-conformance",
9
+ "engine": "playwright",
10
+ "browser": null,
11
+ "platform": "conformance",
12
+ "runtime": "conformance",
13
+ "events": [
14
+ "navigation",
15
+ "interaction",
16
+ "console",
17
+ "pageerror",
18
+ "dialog",
19
+ "requestfailed",
20
+ "download"
21
+ ],
22
+ "dom": {
23
+ "html": true,
24
+ "liveControlState": true,
25
+ "liveState": true,
26
+ "mutations": true,
27
+ "openShadowRoots": true
28
+ },
29
+ "replay": {
30
+ "checkpoints": true,
31
+ "mutations": true,
32
+ "childListPositions": true,
33
+ "liveState": true,
34
+ "identifiers": true
35
+ },
36
+ "privacy": {
37
+ "redactSelectors": [
38
+ "input[type=password]",
39
+ "[data-private]",
40
+ "[data-bc-redact]"
41
+ ],
42
+ "redactAttributes": [
43
+ "authorization",
44
+ "proxy-authorization",
45
+ "cookie",
46
+ "set-cookie",
47
+ "x-api-key",
48
+ "x-auth-token",
49
+ "x-csrf-token"
50
+ ],
51
+ "redactQueryParams": [
52
+ "access_token",
53
+ "api_key",
54
+ "apikey",
55
+ "auth",
56
+ "code",
57
+ "id_token",
58
+ "password",
59
+ "refresh_token",
60
+ "secret",
61
+ "session",
62
+ "signature",
63
+ "token",
64
+ "ticket"
65
+ ],
66
+ "hasCallback": false
67
+ },
68
+ "limits": {
69
+ "maxQueuedMutations": 100
70
+ },
71
+ "counts": {
72
+ "checkpoints": 3,
73
+ "events": 18,
74
+ "mutationBatches": 3
75
+ },
76
+ "dropped": 1
77
+ }
@@ -0,0 +1,2 @@
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":"<p>new
line</p>"}],"removed":[],"index":3}]}
2
+ {"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"}]}
@@ -0,0 +1 @@
1
+ {"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 ✓ 😀"}]}
@@ -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
+ ]