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.
- package/.jscpd.json +2 -1
- package/.prettierignore +1 -0
- package/CHANGELOG.md +159 -0
- package/README.md +216 -32
- package/bin/browser-commander.js +16 -0
- package/eslint.config.js +10 -0
- package/examples/react-test-app/src/App.jsx +24 -23
- package/extension/background.js +14 -0
- package/extension/manifest.json +12 -0
- package/extension/relay-handler.js +353 -0
- package/package.json +21 -4
- package/src/browser/attach/differences.js +131 -0
- package/src/browser/attach/extension-relay.js +402 -0
- package/src/browser/attach/index.js +81 -0
- package/src/browser/attach/snapshot-launch.js +146 -0
- package/src/browser/attach/snapshot.js +374 -0
- package/src/browser/attach/websocket.js +615 -0
- package/src/browser/browser-cookie-cache.js +26 -19
- package/src/browser/browser-cookie-credentials.js +6 -9
- package/src/browser/browser-cookies.js +5 -2
- package/src/browser/cdp-session.js +94 -0
- package/src/browser/connector.js +42 -4
- package/src/browser/debugging-port.js +155 -0
- package/src/browser/launch-options.js +63 -52
- package/src/browser/launch-restrictions.json +147 -0
- package/src/browser/launcher.js +329 -133
- package/src/browser/media.js +5 -0
- package/src/browser/migration/bookmarks.js +73 -0
- package/src/browser/migration/chromium-crypto.js +89 -0
- package/src/browser/migration/cookies.js +155 -0
- package/src/browser/migration/extensions.js +187 -0
- package/src/browser/migration/firefox-bookmarks.js +144 -0
- package/src/browser/migration/firefox-der.js +84 -0
- package/src/browser/migration/firefox-nss.js +281 -0
- package/src/browser/migration/firefox.js +334 -0
- package/src/browser/migration/fs-utils.js +51 -0
- package/src/browser/migration/history.js +103 -0
- package/src/browser/migration/index.js +307 -0
- package/src/browser/migration/os-crypt-keys.js +106 -0
- package/src/browser/migration/passwords.js +171 -0
- package/src/browser/migration/preferences.js +132 -0
- package/src/browser/migration/sqlite-snapshot.js +138 -0
- package/src/browser/open-in-user-browser.js +112 -0
- package/src/browser/parity.js +439 -0
- package/src/browser/profile-directory.js +205 -0
- package/src/browser/real-browser.js +545 -306
- package/src/browser/restrictions.js +148 -0
- package/src/browser/system-browser.js +232 -0
- package/src/browser/webdriver.js +572 -0
- package/src/cli/args.js +300 -0
- package/src/cli/commands.js +332 -0
- package/src/cli/dispatcher.js +99 -0
- package/src/cli/handle-methods.js +136 -0
- package/src/cli/handles.js +273 -0
- package/src/cli/high-level-methods.js +310 -0
- package/src/cli/main.js +67 -0
- package/src/cli/modules.js +100 -0
- package/src/cli/rpc-error.js +77 -0
- package/src/cli/script.js +77 -0
- package/src/cli/serve.js +144 -0
- package/src/cli/sessions.js +199 -0
- package/src/core/constants.js +7 -1
- package/src/core/engine-adapter.js +107 -1
- package/src/core/engine-detection.js +18 -3
- package/src/core/navigation-safety.js +4 -0
- package/src/core/webdriver-conversions.js +347 -0
- package/src/core/webdriver-events.js +229 -0
- package/src/core/webdriver-page.js +716 -0
- package/src/downloads/attach.js +6 -0
- package/src/elements/selectors.js +3 -2
- package/src/elements/visibility.js +4 -0
- package/src/exports.js +61 -2
- package/src/factory.js +19 -4
- package/src/fingerprint/apply.js +6 -0
- package/src/fingerprint/automation-parity.js +7 -8
- package/src/fingerprint/limitations.js +1 -1
- package/src/fingerprint/limitations.json +45 -2
- package/src/parity/harness.js +303 -0
- package/src/parity/probe.js +838 -0
- package/src/parity/version-page.js +144 -0
- package/src/traces/bundle.js +3 -2
- package/src/traces/observers.js +14 -5
- package/src/traces/recorder.js +3 -0
- package/src/traces/viewer.js +16 -8
- package/src/utilities/subprocess.js +199 -0
- package/tests/e2e/api-coverage.e2e.test.js +174 -0
- package/tests/e2e/cli.e2e.test.js +160 -0
- package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
- package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
- package/tests/e2e/measure-parity.e2e.test.js +118 -0
- package/tests/e2e/playwright.e2e.test.js +26 -17
- package/tests/e2e/puppeteer.e2e.test.js +18 -23
- package/tests/e2e/webdriver.e2e.test.js +239 -0
- package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.html +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.png +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.state.json +54 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.png +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.state.json +60 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.png +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.state.json +60 -0
- package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +18 -0
- package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +77 -0
- package/tests/fixtures/traces/conformance/expected/bundle/mutations/0001.ndjson +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/mutations/0002.ndjson +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +309 -0
- package/tests/fixtures/traces/conformance/expected/redaction.json +86 -0
- package/tests/fixtures/traces/conformance/expected/trace.lino +27 -0
- package/tests/fixtures/traces/conformance/scenario.json +249 -0
- package/tests/helpers/cli-fakes.js +214 -0
- package/tests/helpers/declared-api.js +80 -0
- package/tests/helpers/e2e-browser.js +40 -14
- package/tests/helpers/fake-chrome.js +124 -0
- package/tests/helpers/migration-fixtures.js +242 -0
- package/tests/helpers/temp-directory.js +24 -0
- package/tests/helpers/webdriver-mocks.js +183 -0
- package/tests/unit/browser/attach/extension-relay.test.js +360 -0
- package/tests/unit/browser/attach/snapshot-launch.test.js +321 -0
- package/tests/unit/browser/attach/snapshot.test.js +409 -0
- package/tests/unit/browser/attach/websocket.test.js +240 -0
- package/tests/unit/browser/browser-cookie-lock.test.js +102 -0
- package/tests/unit/browser/browser-cookies.test.js +2 -32
- package/tests/unit/browser/cdp-session.test.js +162 -0
- package/tests/unit/browser/connector.test.js +23 -0
- package/tests/unit/browser/launch-building-blocks.test.js +361 -0
- package/tests/unit/browser/launch-options.test.js +41 -22
- package/tests/unit/browser/launcher.test.js +281 -102
- package/tests/unit/browser/media.test.js +13 -1
- package/tests/unit/browser/migration/bookmarks.test.js +69 -0
- package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
- package/tests/unit/browser/migration/cookies.test.js +94 -0
- package/tests/unit/browser/migration/extensions.test.js +104 -0
- package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
- package/tests/unit/browser/migration/firefox.test.js +139 -0
- package/tests/unit/browser/migration/history.test.js +48 -0
- package/tests/unit/browser/migration/index.test.js +117 -0
- package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
- package/tests/unit/browser/migration/passwords.test.js +144 -0
- package/tests/unit/browser/migration/preferences.test.js +77 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
- package/tests/unit/browser/open-in-user-browser.test.js +123 -0
- package/tests/unit/browser/parity.test.js +281 -0
- package/tests/unit/browser/real-browser.test.js +458 -33
- package/tests/unit/browser/storage-state.test.js +2 -0
- package/tests/unit/browser/webdriver.test.js +473 -0
- package/tests/unit/cli/bin.test.js +69 -0
- package/tests/unit/cli/declared-api.test.js +49 -0
- package/tests/unit/cli/dispatcher.test.js +429 -0
- package/tests/unit/cli/handles.test.js +170 -0
- package/tests/unit/cli/main.test.js +437 -0
- package/tests/unit/cli/serve.test.js +167 -0
- package/tests/unit/core/navigation-manager.test.js +11 -1
- package/tests/unit/core/webdriver-conversions.test.js +235 -0
- package/tests/unit/core/webdriver-page.test.js +534 -0
- package/tests/unit/fingerprint/automation-parity.test.js +17 -4
- package/tests/unit/fingerprint/limitations.test.js +1 -1
- package/tests/unit/parity/harness.test.js +138 -0
- package/tests/unit/playwright-protocol-coverage.test.js +231 -0
- package/tests/unit/puppeteer-bindings-coverage.test.js +214 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +27 -0
- package/tests/unit/scripts/feature-parity.test.js +327 -0
- package/tests/unit/traces/conformance.test.js +92 -0
- package/tests/unit/traces/reader.test.js +1 -0
- package/tests/unit/traces/recorder.test.js +1 -0
- package/tests/unit/traces/viewer.test.js +78 -0
- package/tests/unit/utilities/command-stream-only.test.js +28 -0
- 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
|
+
]
|