browser-commander 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jscpd-baseline.json +5 -19
- package/CHANGELOG.md +6 -0
- package/README.md +20 -10
- package/package.json +1 -1
- package/src/browser/browser-profile-files.js +5 -2
- package/src/browser/connector.js +140 -65
- package/src/browser/persistent-session.js +110 -38
- package/src/browser/session-adoption.js +96 -0
- package/src/browser/session-worker.js +12 -0
- package/src/cli/args.js +3 -0
- package/src/cli/commands.js +3 -0
- package/src/cli/high-level-methods.js +7 -0
- package/src/cli/sessions.js +3 -0
- package/src/core/commander-features.js +6 -1
- package/src/core/navigation-safety.js +1 -0
- package/src/core/page-trigger-manager.js +173 -134
- package/src/index.js +2 -0
- package/src/interactions/overlays.js +59 -14
- package/src/traces/bundle.js +41 -3
- package/src/traces/checkpoints.js +7 -3
- package/src/traces/links.js +44 -5
- package/src/traces/mutation-stream.js +8 -2
- package/src/traces/network.js +65 -13
- package/src/traces/page-capture.js +65 -21
- package/src/traces/recorder.js +33 -3
- package/src/traces/redaction.js +89 -6
- package/src/traces/rolling-bundle.js +240 -0
- package/src/traces/rolling.js +10 -113
- package/src/traces/stop.js +1 -0
- package/tests/e2e/issue-160.e2e.test.js +291 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +1 -1
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +1 -1
- package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +2 -2
- package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +4 -0
- package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +1 -1
- package/tests/fixtures/traces/conformance/expected/trace.lino +2 -2
- package/tests/unit/browser/issue-160-connector.test.js +157 -0
- package/tests/unit/core/interrupted-navigation.test.js +32 -0
- package/tests/unit/core/trigger-stop-race.test.js +80 -0
- package/tests/unit/interactions/overlays.test.js +58 -0
- package/tests/unit/traces/issue-160-privacy.test.js +110 -0
- package/tests/unit/traces/issue-160-rotation.test.js +140 -0
- package/tests/unit/traces/network.test.js +55 -3
|
@@ -10,9 +10,9 @@
|
|
|
10
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
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
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}
|
|
13
|
+
{"sequence":13,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"mutations","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","member":"mutations/0001.ndjson","batches":2,"checkpoint":1,"frames":1,"truncated":false}
|
|
14
14
|
{"sequence":14,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"checkpoint","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","index":2,"name":"after click","actor":"automation","reason":"checkpoint","url":"https://example.com/next?token=[redacted]","truncated":true,"members":{"html":"checkpoints/0002.html","state":"checkpoints/0002.state.json","screenshot":"checkpoints/0002.png"}}
|
|
15
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}
|
|
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,"truncated":false}
|
|
17
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
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}
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"privacy": {
|
|
37
37
|
"redactSelectors": [
|
|
38
38
|
"input[type=password]",
|
|
39
|
+
"input[type=hidden]",
|
|
39
40
|
"[data-private]",
|
|
40
41
|
"[data-bc-redact]"
|
|
41
42
|
],
|
|
@@ -61,6 +62,9 @@
|
|
|
61
62
|
"session",
|
|
62
63
|
"signature",
|
|
63
64
|
"token",
|
|
65
|
+
"csrf",
|
|
66
|
+
"xsrf",
|
|
67
|
+
"_xsrf",
|
|
64
68
|
"ticket"
|
|
65
69
|
],
|
|
66
70
|
"hasCallback": false
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
<div class="panel" id="details"></div>
|
|
54
54
|
<div class="panel" id="diff"></div>
|
|
55
55
|
</main>
|
|
56
|
-
<script id="trace-data" type="application/json">{"manifest":{"schemaVersion":2,"format":"browser-commander-trace","mode":"continuous","outcome":"partial","startedAt":"2026-01-01T00:00:00.000Z","stoppedAt":"2026-01-01T00:00:00.000Z","commanderVersion":"0.0.0-conformance","engine":"playwright","browser":null,"platform":"conformance","runtime":"conformance","events":["navigation","interaction","console","pageerror","dialog","requestfailed","download"],"dom":{"html":true,"liveControlState":true,"liveState":true,"mutations":true,"openShadowRoots":true},"replay":{"checkpoints":true,"mutations":true,"childListPositions":true,"liveState":true,"identifiers":true},"privacy":{"redactSelectors":["input[type=password]","[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>
|
|
56
|
+
<script id="trace-data" type="application/json">{"manifest":{"schemaVersion":2,"format":"browser-commander-trace","mode":"continuous","outcome":"partial","startedAt":"2026-01-01T00:00:00.000Z","stoppedAt":"2026-01-01T00:00:00.000Z","commanderVersion":"0.0.0-conformance","engine":"playwright","browser":null,"platform":"conformance","runtime":"conformance","events":["navigation","interaction","console","pageerror","dialog","requestfailed","download"],"dom":{"html":true,"liveControlState":true,"liveState":true,"mutations":true,"openShadowRoots":true},"replay":{"checkpoints":true,"mutations":true,"childListPositions":true,"liveState":true,"identifiers":true},"privacy":{"redactSelectors":["input[type=password]","input[type=hidden]","[data-private]","[data-bc-redact]"],"redactAttributes":["authorization","proxy-authorization","cookie","set-cookie","x-api-key","x-auth-token","x-csrf-token"],"redactQueryParams":["access_token","api_key","apikey","auth","code","id_token","password","refresh_token","secret","session","signature","token","csrf","xsrf","_xsrf","ticket"],"hasCallback":false},"limits":{"maxQueuedMutations":100},"counts":{"checkpoints":3,"events":18,"mutationBatches":3},"dropped":1},"events":[{"sequence":1,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"trace.start","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-1","mode":"continuous","engine":"playwright","dom":{"html":true,"liveControlState":true,"liveState":true,"mutations":true,"openShadowRoots":true},"events":["navigation","interaction","console","pageerror","dialog","requestfailed","download"],"initialCheckpoint":true},{"sequence":2,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"checkpoint","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-1","index":1,"name":"initial","actor":"recorder","reason":"initial","url":"https://[redacted]:[redacted]@example.com/start?ticket=[redacted]&lang=en#access_token=[redacted]&view=1","truncated":false,"members":{"html":"checkpoints/0001.html","state":"checkpoints/0001.state.json","screenshot":"checkpoints/0001.png"}},{"sequence":3,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"navigation","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","phase":"framenavigated","mainFrame":true,"url":"https://example.com/next?token=[redacted]&q=a+b"},{"sequence":4,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"navigation","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","phase":"framenavigated","mainFrame":false,"url":"https://ads.example.com/frame"},{"sequence":5,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"console","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","level":"log","text":"key [redacted] loaded\n\ttab \"and\" 'quotes'"},{"sequence":6,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"pageerror","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","message":"boom: (unbalanced","stack":"Error: boom\n at main.js:1:2"},{"sequence":7,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"requestfailed","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","url":"https://api.example.com/v1?api_key=[redacted]&page=2","method":"POST","failure":"net::ERR_FAILED"},{"sequence":8,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"dialog","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","type":"confirm","message":"Continue? #yes"},{"sequence":9,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"download","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","phase":"completed","id":"download-1","suggestedFilename":"report final.csv","path":"/downloads/report final.csv","checksum":"sha256:00ff","bytes":42,"url":"https://files.example.com/r.csv?signature=[redacted]","failure":null},{"sequence":10,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"interaction","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","actionId":"trace-1-action-1","action":"goto","target":"https://example.com/next?code=abc","durationMs":0,"ok":true},{"sequence":11,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"interaction","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","actionId":"trace-1-action-2","action":"clickButton","target":"#go","durationMs":0,"ok":false,"error":"button #go is not visible"},{"sequence":12,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"dropped","reason":"size-limit","member":"mutations","detail":"2 records over the in-page queue limit"},{"sequence":13,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"mutations","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","member":"mutations/0001.ndjson","batches":2,"checkpoint":1,"frames":1,"truncated":false},{"sequence":14,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"checkpoint","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","index":2,"name":"after click","actor":"automation","reason":"checkpoint","url":"https://example.com/next?token=[redacted]","truncated":true,"members":{"html":"checkpoints/0002.html","state":"checkpoints/0002.state.json","screenshot":"checkpoints/0002.png"}},{"sequence":15,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"interaction","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","action":"note","actor":"caller","authorization":"[redacted]","detail":"uses [redacted]","pageUrl":"https://example.com/?session=[redacted]","count":3,"ratio":0.25,"whole":2,"nested":{"list":[1,"two",null,true]}},{"sequence":16,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"mutations","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","member":"mutations/0002.ndjson","batches":1,"checkpoint":2,"frames":1,"truncated":false},{"sequence":17,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"checkpoint","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","index":3,"name":"final","actor":"caller","reason":"failure","url":"https://example.com/next?token=[redacted]","truncated":true,"members":{"html":"checkpoints/0003.html","state":"checkpoints/0003.state.json","screenshot":"checkpoints/0003.png"}},{"sequence":18,"at":"2026-01-01T00:00:00.000Z","monotonicMs":1235,"kind":"trace.stop","traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","discarded":false}],"checkpoints":[{"index":1,"name":"initial","actor":"recorder","reason":"initial","url":"https://[redacted]:[redacted]@example.com/start?ticket=[redacted]&lang=en#access_token=[redacted]&view=1","at":"2026-01-01T00:00:00.000Z","sequence":2,"members":{"html":"checkpoints/0001.html","state":"checkpoints/0001.state.json","screenshot":"checkpoints/0001.png"}},{"index":2,"name":"after click","actor":"automation","reason":"checkpoint","url":"https://example.com/next?token=[redacted]","at":"2026-01-01T00:00:00.000Z","sequence":14,"members":{"html":"checkpoints/0002.html","state":"checkpoints/0002.state.json","screenshot":"checkpoints/0002.png"}},{"index":3,"name":"final","actor":"caller","reason":"failure","url":"https://example.com/next?token=[redacted]","at":"2026-01-01T00:00:00.000Z","sequence":17,"members":{"html":"checkpoints/0003.html","state":"checkpoints/0003.state.json","screenshot":"checkpoints/0003.png"}}],"html":{"1":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\u003chead\u003e\u003ctitle\u003eStart\u003c/title\u003e\u003c/head\u003e\u003cbody\u003e\u003cform\u003e\u003cinput name=\"user\" value=\"alice\"\u003e\u003cinput type=\"checkbox\" name=\"agree\"\u003e\u003cselect name=\"plan\"\u003e\u003coption\u003ea\u003c/option\u003e\u003c/select\u003e\u003c/form\u003e\u003c/body\u003e\u003c/html\u003e","2":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\u003cbody\u003e\u003cform\u003e\u003cinput name=\"user\" value=\"bob\"\u003e\u003ctextarea\u003e[redacted] note\u003c/textarea\u003e\u003c/form\u003e\u003c/body\u003e\u003c/html\u003e","3":"\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\u003cbody\u003e\u003cform\u003e\u003cinput name=\"user\" value=\"bob\"\u003e\u003ctextarea\u003e[redacted] note\u003c/textarea\u003e\u003c/form\u003e\u003c/body\u003e\u003c/html\u003e"},"state":{"1":{"url":"https://[redacted]:[redacted]@example.com/start?ticket=[redacted]&lang=en#access_token=[redacted]&view=1","title":"Start – “quoted” 'single' \\ back","readyState":"complete","scroll":{"x":0,"y":12.5},"viewport":{"width":1280,"height":720},"activeElement":null,"controls":[{"path":"form \u003e input:nth-of-type(1)","tag":"input","type":"text","name":"user","id":null,"focused":false,"value":"alice","redacted":false},{"path":"form \u003e input:nth-of-type(2)","tag":"input","type":"checkbox","name":"agree","id":null,"focused":false,"value":"on","checked":false,"redacted":false},{"path":"form \u003e select","tag":"select","type":null,"name":"plan","id":null,"focused":false,"value":"a","selected":[0],"redacted":false}],"frames":[],"name":"initial","actor":"recorder","reason":"initial"},"2":{"url":"https://example.com/next?token=[redacted]","title":"Next","readyState":"interactive","scroll":{"x":0,"y":0},"viewport":{"width":1280,"height":720},"activeElement":"form \u003e textarea","controls":[{"path":"form \u003e input:nth-of-type(1)","tag":"input","type":"text","name":"user","id":null,"focused":false,"value":"bob","redacted":false},{"path":"form \u003e input:nth-of-type(2)","tag":"input","type":"checkbox","name":"agree","id":null,"focused":false,"value":"on","checked":true,"redacted":false},{"path":"form \u003e textarea","tag":"textarea","type":null,"name":null,"id":"note","focused":true,"value":"[redacted] note","scroll":{"x":0,"y":3},"redacted":false}],"frames":[{"url":"https://ads.example.com/frame","crossOrigin":true}],"name":"after click","actor":"automation","reason":"checkpoint"},"3":{"url":"https://example.com/next?token=[redacted]","title":"Next","readyState":"interactive","scroll":{"x":0,"y":0},"viewport":{"width":1280,"height":720},"activeElement":"form \u003e textarea","controls":[{"path":"form \u003e input:nth-of-type(1)","tag":"input","type":"text","name":"user","id":null,"focused":false,"value":"bob","redacted":false},{"path":"form \u003e input:nth-of-type(2)","tag":"input","type":"checkbox","name":"agree","id":null,"focused":false,"value":"on","checked":true,"redacted":false},{"path":"form \u003e textarea","tag":"textarea","type":null,"name":null,"id":"note","focused":true,"value":"[redacted] note","scroll":{"x":0,"y":3},"redacted":false}],"frames":[{"url":"https://ads.example.com/frame","crossOrigin":true}],"name":"final","actor":"caller","reason":"failure"}},"mutations":{"1":[{"traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","frameId":"main","mainFrame":true,"at":1767225600100,"records":[{"type":"live-state","target":2,"property":"value","value":"bo"},{"type":"childList","target":1,"added":[{"id":9,"html":"\u003cp\u003enew\u2028line\u003c/p\u003e"}],"removed":[],"index":3}]},{"traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","frameId":"main","mainFrame":true,"at":1767225600200,"records":[{"type":"attributes","target":4,"name":"class","value":"busy"}]}],"2":[{"traceId":"trace-1","browserContextId":"context-1","pageId":"page-1","navigationId":"nav-2","frameId":"main","mainFrame":true,"at":1767225600300,"records":[{"type":"characterData","target":7,"value":"done ✓ 😀"}]}],"3":[]},"diffs":{"1":[],"2":[{"path":"form \u003e input:nth-of-type(1)","change":"changed","before":"alice","after":"bob"},{"path":"form \u003e input:nth-of-type(2)","change":"changed","before":false,"after":true},{"path":"form \u003e textarea","change":"added","after":"[redacted] note"},{"path":"form \u003e select","change":"removed","before":"a"}],"3":[]},"elided":[]}</script>
|
|
57
57
|
<script>
|
|
58
58
|
const trace = JSON.parse(document.getElementById('trace-data').textContent);
|
|
59
59
|
const timeline = document.getElementById('timeline');
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
(timeline: (sequence: 10) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: interaction) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: automation) (action: goto) (target: 'https://example.com/next?code=abc') (outcome: ok) (actionId: trace-1-action-1) (durationMs: 0) (ok: true))
|
|
13
13
|
(timeline: (sequence: 11) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: interaction) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: automation) (action: clickButton) (target: '#go') (outcome: failed) (actionId: trace-1-action-2) (durationMs: 0) (ok: false) (error: 'button #go is not visible'))
|
|
14
14
|
(timeline: (sequence: 12) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: dropped) (actor: recorder) (target: mutations) (outcome: dropped) (reason: size-limit) (member: mutations) (detail: '2 records over the in-page queue limit'))
|
|
15
|
-
(timeline: (sequence: 13) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: mutations) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: recorder) (target: mutations/0001.ndjson) (outcome: recorded) (member: mutations/0001.ndjson) (batches: 2) (checkpoint: 1) (frames: 1))
|
|
15
|
+
(timeline: (sequence: 13) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: mutations) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: recorder) (target: mutations/0001.ndjson) (outcome: recorded) (member: mutations/0001.ndjson) (batches: 2) (checkpoint: 1) (frames: 1) (truncated: false))
|
|
16
16
|
(timeline: (sequence: 14) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: checkpoint) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: automation) (target: 'https://example.com/next?token=[redacted]') (outcome: partial) (index: 2) (name: 'after click') (reason: checkpoint) (url: 'https://example.com/next?token=[redacted]') (truncated: true))
|
|
17
17
|
(checkpoint: (index: 2) (sequence: 14) (at: '2026-01-01T00:00:00.000Z') (name: 'after click') (actor: automation) (reason: checkpoint) (page: page-1) (navigation: nav-2) (url: 'https://example.com/next?token=[redacted]') (outcome: partial) (html: checkpoints/0002.html) (state: checkpoints/0002.state.json) (screenshot: checkpoints/0002.png))
|
|
18
18
|
(timeline: (sequence: 15) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: interaction) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: caller) (action: note) (outcome: recorded) (authorization: [redacted]) (detail: 'uses [redacted]') (pageUrl: 'https://example.com/?session=[redacted]') (count: 3) (ratio: 0.25) (whole: 2) (nested: '{"list":[1,"two",null,true]}'))
|
|
19
|
-
(timeline: (sequence: 16) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: mutations) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: recorder) (target: mutations/0002.ndjson) (outcome: recorded) (member: mutations/0002.ndjson) (batches: 1) (checkpoint: 2) (frames: 1))
|
|
19
|
+
(timeline: (sequence: 16) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: mutations) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: recorder) (target: mutations/0002.ndjson) (outcome: recorded) (member: mutations/0002.ndjson) (batches: 1) (checkpoint: 2) (frames: 1) (truncated: false))
|
|
20
20
|
(timeline: (sequence: 17) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: checkpoint) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: caller) (target: 'https://example.com/next?token=[redacted]') (outcome: partial) (index: 3) (name: final) (reason: failure) (url: 'https://example.com/next?token=[redacted]') (truncated: true))
|
|
21
21
|
(checkpoint: (index: 3) (sequence: 17) (at: '2026-01-01T00:00:00.000Z') (name: final) (actor: caller) (reason: failure) (page: page-1) (navigation: nav-2) (url: 'https://example.com/next?token=[redacted]') (outcome: partial) (html: checkpoints/0003.html) (state: checkpoints/0003.state.json) (screenshot: checkpoints/0003.png))
|
|
22
22
|
(timeline: (sequence: 18) (at: '2026-01-01T00:00:00.000Z') (monotonicMs: 1235) (kind: trace.stop) (trace: trace-1) (context: context-1) (page: page-1) (navigation: nav-2) (actor: recorder) (outcome: recorded) (discarded: false))
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { it } from 'node:test';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { physicalPath } from '../../../src/browser/browser-profile-files.js';
|
|
5
|
+
import {
|
|
6
|
+
matchesSessionProcess,
|
|
7
|
+
processArguments,
|
|
8
|
+
} from '../../../src/browser/session-adoption.js';
|
|
9
|
+
import {
|
|
10
|
+
connectBrowserWithDependencies,
|
|
11
|
+
pickForegroundPage,
|
|
12
|
+
} from '../../../src/browser/connector.js';
|
|
13
|
+
|
|
14
|
+
it('keeps plain profile paths when macOS privacy protection denies resolution', () => {
|
|
15
|
+
for (const code of ['EPERM', 'EACCES']) {
|
|
16
|
+
assert.equal(
|
|
17
|
+
physicalPath('/tmp/dedicated-profile', () => {
|
|
18
|
+
throw Object.assign(new Error('protected'), { code });
|
|
19
|
+
}),
|
|
20
|
+
path.resolve('/tmp/dedicated-profile')
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('verifies adoption profile, port and browser process across process-list formats', () => {
|
|
26
|
+
const options = {
|
|
27
|
+
userDataDir: '/tmp/managed profile',
|
|
28
|
+
remoteDebuggingPort: 9222,
|
|
29
|
+
};
|
|
30
|
+
const commands = [
|
|
31
|
+
'/usr/bin/google-chrome --user-data-dir="/tmp/managed profile" --remote-debugging-port=9222',
|
|
32
|
+
'/opt/google/chrome/chrome --user-data-dir="/tmp/managed profile" --remote-debugging-port=9222',
|
|
33
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --user-data-dir=/tmp/managed profile --remote-debugging-port=9222',
|
|
34
|
+
];
|
|
35
|
+
for (const command of commands) {
|
|
36
|
+
assert.equal(
|
|
37
|
+
matchesSessionProcess(processArguments(command), options),
|
|
38
|
+
true
|
|
39
|
+
);
|
|
40
|
+
for (const changed of [
|
|
41
|
+
command.replace('9222', '9223'),
|
|
42
|
+
command.replace('managed profile', 'other profile'),
|
|
43
|
+
`${command} --type=renderer`,
|
|
44
|
+
]) {
|
|
45
|
+
assert.equal(
|
|
46
|
+
matchesSessionProcess(processArguments(changed), options),
|
|
47
|
+
false
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
assert.equal(
|
|
52
|
+
matchesSessionProcess(
|
|
53
|
+
[
|
|
54
|
+
'/tmp/fake-chrome-process',
|
|
55
|
+
'--user-data-dir=/tmp/managed profile',
|
|
56
|
+
'--remote-debugging-port=9222',
|
|
57
|
+
],
|
|
58
|
+
options
|
|
59
|
+
),
|
|
60
|
+
false
|
|
61
|
+
);
|
|
62
|
+
const windows = processArguments(
|
|
63
|
+
'"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" --user-data-dir="C:\\managed profile" --remote-debugging-port=9222'
|
|
64
|
+
);
|
|
65
|
+
assert.equal(
|
|
66
|
+
windows[0],
|
|
67
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
|
|
68
|
+
);
|
|
69
|
+
assert.equal(windows[1], '--user-data-dir=C:\\managed profile');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('falls back from remembered targets and honors ranked URL matchers', async () => {
|
|
73
|
+
const pages = [
|
|
74
|
+
'https://example.test/first',
|
|
75
|
+
'https://example.test/preferred',
|
|
76
|
+
].map((url) => ({ url: () => url, evaluate: async () => 'visible' }));
|
|
77
|
+
assert.equal(
|
|
78
|
+
await pickForegroundPage(pages, {
|
|
79
|
+
targetId: 'stale',
|
|
80
|
+
fallback: true,
|
|
81
|
+
url: [/preferred/, /first/],
|
|
82
|
+
}),
|
|
83
|
+
pages[1]
|
|
84
|
+
);
|
|
85
|
+
assert.equal(
|
|
86
|
+
await pickForegroundPage(pages, { targetId: 'stale', fallback: true }),
|
|
87
|
+
pages[0]
|
|
88
|
+
);
|
|
89
|
+
assert.equal(
|
|
90
|
+
await pickForegroundPage([], { targetId: 'stale', fallback: true }),
|
|
91
|
+
undefined
|
|
92
|
+
);
|
|
93
|
+
await assert.rejects(
|
|
94
|
+
pickForegroundPage(pages, { targetId: 'explicit' }),
|
|
95
|
+
/No tab matches/
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('disconnects attached Playwright and Puppeteer browsers on selection failure', async () => {
|
|
100
|
+
for (const engine of ['playwright', 'puppeteer']) {
|
|
101
|
+
let detached = 0;
|
|
102
|
+
const browser = {
|
|
103
|
+
contexts: () => [{ pages: () => [] }],
|
|
104
|
+
pages: async () => [],
|
|
105
|
+
close: async () => detached++,
|
|
106
|
+
disconnect: async () => detached++,
|
|
107
|
+
};
|
|
108
|
+
await assert.rejects(
|
|
109
|
+
connectBrowserWithDependencies(
|
|
110
|
+
{ engine, cdpEndpoint: 'http://127.0.0.1:9222', targetId: 'missing' },
|
|
111
|
+
{
|
|
112
|
+
loadPlaywright: async () => ({
|
|
113
|
+
chromium: { connectOverCDP: async () => browser },
|
|
114
|
+
}),
|
|
115
|
+
loadPuppeteer: async () => ({ connect: async () => browser }),
|
|
116
|
+
}
|
|
117
|
+
),
|
|
118
|
+
/No tab matches/
|
|
119
|
+
);
|
|
120
|
+
assert.equal(detached, 1);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('forwards noDefaults and recovers only the zero-page context-management error', async () => {
|
|
125
|
+
const calls = [];
|
|
126
|
+
const page = { url: () => 'about:blank' };
|
|
127
|
+
const browser = {
|
|
128
|
+
contexts: () => [{ pages: () => [], newPage: async () => page }],
|
|
129
|
+
close: async () => {},
|
|
130
|
+
};
|
|
131
|
+
const loadPlaywright = async () => ({
|
|
132
|
+
chromium: {
|
|
133
|
+
connectOverCDP: async (_, options) => {
|
|
134
|
+
calls.push(options);
|
|
135
|
+
if (calls.length === 1 && options.noDefaults === undefined) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
'Protocol error (Browser.setDownloadBehavior): Browser context management is not supported.'
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return browser;
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
const result = await connectBrowserWithDependencies(
|
|
145
|
+
{ cdpEndpoint: 'http://127.0.0.1:9222' },
|
|
146
|
+
{ loadPlaywright }
|
|
147
|
+
);
|
|
148
|
+
assert.equal(result.page, page);
|
|
149
|
+
assert.deepEqual(calls, [{}, { noDefaults: true }]);
|
|
150
|
+
for (const noDefaults of [false, true]) {
|
|
151
|
+
await connectBrowserWithDependencies(
|
|
152
|
+
{ cdpEndpoint: 'http://127.0.0.1:9222', noDefaults },
|
|
153
|
+
{ loadPlaywright }
|
|
154
|
+
);
|
|
155
|
+
assert.equal(calls.at(-1).noDefaults, noDefaults);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { isNavigationError } from '../../../src/core/navigation-safety.js';
|
|
4
|
+
import { createManagedNavigator } from '../../../src/core/managed-navigation.js';
|
|
5
|
+
|
|
6
|
+
it('classifies net::ERR_ABORTED as an interrupted navigation outcome', async () => {
|
|
7
|
+
const error = new Error(
|
|
8
|
+
'page.goto: net::ERR_ABORTED at https://example.test/a'
|
|
9
|
+
);
|
|
10
|
+
assert.equal(isNavigationError(error), true);
|
|
11
|
+
const goto = createManagedNavigator({
|
|
12
|
+
page: {
|
|
13
|
+
goto: async () => {
|
|
14
|
+
throw error;
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
state: {},
|
|
18
|
+
config: {},
|
|
19
|
+
triggerNavigationStart: async () => {},
|
|
20
|
+
updateCurrentUrl() {},
|
|
21
|
+
abandonNavigation() {},
|
|
22
|
+
waitForReady: async () => ({ ready: true }),
|
|
23
|
+
});
|
|
24
|
+
assert.equal(
|
|
25
|
+
(await goto({ url: 'https://example.test/a', returnOutcome: true })).status,
|
|
26
|
+
'interrupted'
|
|
27
|
+
);
|
|
28
|
+
assert.equal(
|
|
29
|
+
isNavigationError(new Error('net::ERR_NAME_NOT_RESOLVED')),
|
|
30
|
+
false
|
|
31
|
+
);
|
|
32
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { createPageTriggerManager } from '../../../src/core/page-trigger-manager.js';
|
|
5
|
+
|
|
6
|
+
const flush = () => new Promise((resolve) => globalThis.setImmediate(resolve));
|
|
7
|
+
|
|
8
|
+
for (const concurrency of ['skip', 'queue', 'restart']) {
|
|
9
|
+
it(`retains DOM-ready work during stop (${concurrency})`, async () => {
|
|
10
|
+
const navigationManager = new EventEmitter();
|
|
11
|
+
const page = new EventEmitter();
|
|
12
|
+
page.url = () => 'https://example.test/new';
|
|
13
|
+
const manager = createPageTriggerManager({
|
|
14
|
+
navigationManager,
|
|
15
|
+
log: { debug() {} },
|
|
16
|
+
stopGraceMs: 20,
|
|
17
|
+
});
|
|
18
|
+
const calls = [];
|
|
19
|
+
let release;
|
|
20
|
+
manager.pageTrigger({
|
|
21
|
+
condition: () => true,
|
|
22
|
+
readyOn: 'domcontentloaded',
|
|
23
|
+
concurrency,
|
|
24
|
+
action: async (ctx) => {
|
|
25
|
+
calls.push(ctx.url);
|
|
26
|
+
if (calls.length === 1) {
|
|
27
|
+
await new Promise((resolve) => {
|
|
28
|
+
release = resolve;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
manager.initialize({ page });
|
|
34
|
+
page.emit('domcontentloaded');
|
|
35
|
+
await flush();
|
|
36
|
+
const stopping = manager.stopCurrentAction();
|
|
37
|
+
page.emit('domcontentloaded');
|
|
38
|
+
await stopping;
|
|
39
|
+
assert.equal(calls.length, 1, 'grace expiry must never overlap actions');
|
|
40
|
+
release();
|
|
41
|
+
await flush();
|
|
42
|
+
await flush();
|
|
43
|
+
assert.equal(calls.length, 2, 'ready event survives grace expiry');
|
|
44
|
+
await manager.destroy();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
it('queue coalesces repeated events and unregister cancels pending work', async () => {
|
|
49
|
+
const nav = new EventEmitter();
|
|
50
|
+
const page = new EventEmitter();
|
|
51
|
+
page.url = () => 'https://example.test/';
|
|
52
|
+
const manager = createPageTriggerManager({
|
|
53
|
+
navigationManager: nav,
|
|
54
|
+
log: { debug() {} },
|
|
55
|
+
});
|
|
56
|
+
let release;
|
|
57
|
+
let count = 0;
|
|
58
|
+
const remove = manager.pageTrigger({
|
|
59
|
+
condition: () => true,
|
|
60
|
+
concurrency: 'queue',
|
|
61
|
+
action: async () => {
|
|
62
|
+
count++;
|
|
63
|
+
await new Promise((resolve) => {
|
|
64
|
+
release = resolve;
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
manager.initialize({ page });
|
|
69
|
+
nav.emit('onPageReady', { url: page.url() });
|
|
70
|
+
await flush();
|
|
71
|
+
for (let i = 0; i < 20; i++) {
|
|
72
|
+
nav.emit('onPageReady', { url: page.url() });
|
|
73
|
+
}
|
|
74
|
+
await flush();
|
|
75
|
+
remove();
|
|
76
|
+
release();
|
|
77
|
+
await flush();
|
|
78
|
+
assert.equal(count, 1);
|
|
79
|
+
await manager.destroy();
|
|
80
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { dismissOverlays } from '../../../src/interactions/overlays.js';
|
|
4
|
+
|
|
5
|
+
it('dismisses the first visible match and reports errors without failing the interaction', async () => {
|
|
6
|
+
const clicks = [],
|
|
7
|
+
reports = [];
|
|
8
|
+
const targets = [
|
|
9
|
+
{ isVisible: async () => false, click: async () => clicks.push('hidden') },
|
|
10
|
+
{
|
|
11
|
+
isVisible: async () => true,
|
|
12
|
+
click: async () => {
|
|
13
|
+
clicks.push('visible');
|
|
14
|
+
throw new Error('detached');
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
const locator = {
|
|
19
|
+
first: () => targets[0],
|
|
20
|
+
count: async () => targets.length,
|
|
21
|
+
nth: (i) => targets[i],
|
|
22
|
+
};
|
|
23
|
+
await dismissOverlays({
|
|
24
|
+
page: { locator: () => locator },
|
|
25
|
+
engine: 'playwright',
|
|
26
|
+
overlays: ['.dismiss'],
|
|
27
|
+
onDismiss: (report) => reports.push(report),
|
|
28
|
+
});
|
|
29
|
+
assert.deepEqual(clicks, ['visible']);
|
|
30
|
+
assert.equal(reports[0].status, 'error');
|
|
31
|
+
assert.equal(reports[0].error.message, 'detached');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('reports successful dismissal and isolates reporting callback errors', async () => {
|
|
35
|
+
let clicked = false;
|
|
36
|
+
const target = {
|
|
37
|
+
isVisible: async () => true,
|
|
38
|
+
click: async () => {
|
|
39
|
+
clicked = true;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const reports = [];
|
|
43
|
+
await dismissOverlays({
|
|
44
|
+
page: { locator: () => ({ count: async () => 1, nth: () => target }) },
|
|
45
|
+
engine: 'playwright',
|
|
46
|
+
overlays: [
|
|
47
|
+
{
|
|
48
|
+
selector: '.dismiss',
|
|
49
|
+
onDismiss: (result) => {
|
|
50
|
+
reports.push(result);
|
|
51
|
+
throw new Error('reporting');
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
assert.equal(clicked, true);
|
|
57
|
+
assert.equal(reports[0].status, 'dismissed');
|
|
58
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { startTrace } from '../../../src/traces/recorder.js';
|
|
7
|
+
import {
|
|
8
|
+
normalizePrivacyOptions,
|
|
9
|
+
redactValue,
|
|
10
|
+
redactHeaders,
|
|
11
|
+
redactUrl,
|
|
12
|
+
} from '../../../src/traces/redaction.js';
|
|
13
|
+
import {
|
|
14
|
+
createFakePage,
|
|
15
|
+
createFakeCommander,
|
|
16
|
+
makeSnapshot,
|
|
17
|
+
} from '../../helpers/trace-fixtures.js';
|
|
18
|
+
|
|
19
|
+
it('redacts JSON, form, multipart fields and credential header/query names', () => {
|
|
20
|
+
const privacy = normalizePrivacyOptions();
|
|
21
|
+
const cases = [
|
|
22
|
+
{
|
|
23
|
+
postData: JSON.stringify({
|
|
24
|
+
password: 'PRIVATE_A',
|
|
25
|
+
nested: { _xsrf: 'PRIVATE_B', otp: 123456 },
|
|
26
|
+
public: 'visible',
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
{ postData: 'public=visible&password=PRIVATE_A&_xsrf=PRIVATE_B' },
|
|
30
|
+
{
|
|
31
|
+
postData:
|
|
32
|
+
'--boundary\r\nContent-Disposition: form-data; name="password"\r\n\r\nPRIVATE_A\r\n--boundary--\r\n',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
body: { data: JSON.stringify({ csrf: 'PRIVATE_B', public: 'visible' }) },
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
for (const value of cases) {
|
|
39
|
+
const result = JSON.stringify(redactValue(value, privacy));
|
|
40
|
+
assert.doesNotMatch(result, /PRIVATE_[AB]|123456/);
|
|
41
|
+
}
|
|
42
|
+
assert.equal(
|
|
43
|
+
redactHeaders({ 'X-Xsrftoken': 'PRIVATE_A' }, privacy)['X-Xsrftoken'],
|
|
44
|
+
'[redacted]'
|
|
45
|
+
);
|
|
46
|
+
assert.doesNotMatch(
|
|
47
|
+
redactUrl('https://example.test/?xsrf=PRIVATE_A&csrf=PRIVATE_B', privacy),
|
|
48
|
+
/PRIVATE/
|
|
49
|
+
);
|
|
50
|
+
assert.ok(privacy.redactSelectors.includes('input[type=hidden]'));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('redacts checkpoint HTML and mutations before bundle and incremental links writes', async () => {
|
|
54
|
+
const output = await fs.mkdtemp(path.join(os.tmpdir(), 'bc-private-'));
|
|
55
|
+
const page = createFakePage({
|
|
56
|
+
snapshot: makeSnapshot({ html: '<div>public PRIVATE_SENTINEL</div>' }),
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
const trace = await startTrace({
|
|
60
|
+
commander: createFakeCommander(page),
|
|
61
|
+
output,
|
|
62
|
+
mode: 'continuous',
|
|
63
|
+
screenshots: false,
|
|
64
|
+
links: { output: path.join(output, 'trace.lino'), dom: 'full' },
|
|
65
|
+
privacy: { redactPatterns: [/PRIVATE_SENTINEL/g] },
|
|
66
|
+
});
|
|
67
|
+
page.queueMutations([
|
|
68
|
+
{
|
|
69
|
+
records: [
|
|
70
|
+
{
|
|
71
|
+
kind: 'attribute',
|
|
72
|
+
target: { path: '#public' },
|
|
73
|
+
value: 'public PRIVATE_SENTINEL',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
]);
|
|
78
|
+
const cp = await trace.checkpoint('public');
|
|
79
|
+
await trace.stop();
|
|
80
|
+
assert.doesNotMatch(
|
|
81
|
+
await fs.readFile(path.join(output, cp.members.html), 'utf8'),
|
|
82
|
+
/PRIVATE_SENTINEL/
|
|
83
|
+
);
|
|
84
|
+
assert.doesNotMatch(
|
|
85
|
+
await fs.readFile(path.join(output, 'trace.lino'), 'utf8'),
|
|
86
|
+
/PRIVATE_SENTINEL/
|
|
87
|
+
);
|
|
88
|
+
const mutations = await fs.readFile(
|
|
89
|
+
path.join(output, 'mutations/0001.ndjson'),
|
|
90
|
+
'utf8'
|
|
91
|
+
);
|
|
92
|
+
assert.doesNotMatch(mutations, /PRIVATE_SENTINEL/);
|
|
93
|
+
assert.match(mutations, /public/);
|
|
94
|
+
} finally {
|
|
95
|
+
await fs.rm(output, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('redacts escaped script values, numeric OTPs and multipart file fields', () => {
|
|
100
|
+
const privacy = normalizePrivacyOptions();
|
|
101
|
+
for (const postData of [
|
|
102
|
+
'<script>const xsrfToken="PRIVATE_A\\"PRIVATE_B"; const otp=123456;</script>',
|
|
103
|
+
'--b\r\nContent-Disposition: form-data; name="password"; filename="public.txt"\r\n\r\nPRIVATE_A\r\n--b--\r\n',
|
|
104
|
+
]) {
|
|
105
|
+
assert.doesNotMatch(
|
|
106
|
+
JSON.stringify(redactValue({ postData }, privacy)),
|
|
107
|
+
/PRIVATE_[AB]|123456/
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
});
|