browser-commander 0.27.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.
Files changed (93) hide show
  1. package/.jscpd-baseline.json +5 -19
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +23 -9
  4. package/eslint.config.js +11 -0
  5. package/package.json +5 -2
  6. package/src/browser/browser-profile-files.js +5 -2
  7. package/src/browser/connector.js +215 -36
  8. package/src/browser/launch-options.js +2 -1
  9. package/src/browser/launch-restrictions.json +33 -2
  10. package/src/browser/launcher.js +26 -1
  11. package/src/browser/persistent-session.js +432 -0
  12. package/src/browser/profile-directory.js +20 -0
  13. package/src/browser/real-browser.js +7 -2
  14. package/src/browser/restrictions.js +13 -4
  15. package/src/browser/session-adoption.js +96 -0
  16. package/src/browser/session-watchdog.js +71 -0
  17. package/src/browser/session-worker.js +82 -0
  18. package/src/browser/webdriver.js +4 -0
  19. package/src/capture/encoding.js +327 -0
  20. package/src/capture/errors.js +12 -0
  21. package/src/capture/files.js +9 -0
  22. package/src/capture/index.js +5 -0
  23. package/src/capture/recording.js +136 -0
  24. package/src/capture/screenshot.js +222 -0
  25. package/src/capture/video.js +186 -0
  26. package/src/capture/viewport.js +68 -0
  27. package/src/capture/worker.js +22 -0
  28. package/src/cli/args.js +69 -1
  29. package/src/cli/commands.js +116 -2
  30. package/src/cli/high-level-methods.js +47 -3
  31. package/src/cli/sessions.js +14 -3
  32. package/src/core/commander-features.js +110 -0
  33. package/src/core/constants.js +1 -0
  34. package/src/core/navigation-manager.js +4 -2
  35. package/src/core/navigation-readiness.js +30 -2
  36. package/src/core/navigation-safety.js +52 -0
  37. package/src/core/page-trigger-manager.js +280 -156
  38. package/src/core/readiness.js +10 -2
  39. package/src/elements/locators.js +26 -32
  40. package/src/elements/selectors.js +37 -81
  41. package/src/elements/visibility.js +32 -10
  42. package/src/factory.js +13 -12
  43. package/src/index.js +4 -0
  44. package/src/interactions/click.js +44 -0
  45. package/src/interactions/overlays.js +73 -0
  46. package/src/traces/bundle.js +55 -8
  47. package/src/traces/checkpoints.js +103 -0
  48. package/src/traces/links.js +157 -2
  49. package/src/traces/mutation-stream.js +9 -2
  50. package/src/traces/network.js +309 -0
  51. package/src/traces/page-capture.js +203 -68
  52. package/src/traces/reader.js +8 -10
  53. package/src/traces/recorder.js +253 -247
  54. package/src/traces/redaction.js +89 -6
  55. package/src/traces/render.js +168 -0
  56. package/src/traces/rolling-bundle.js +240 -0
  57. package/src/traces/rolling.js +77 -0
  58. package/src/traces/start-cleanup.js +15 -0
  59. package/src/traces/stop.js +160 -0
  60. package/src/traces/storage.js +43 -0
  61. package/src/utilities/subprocess.js +32 -0
  62. package/tests/e2e/cli.e2e.test.js +20 -1
  63. package/tests/e2e/click-readiness.e2e.test.js +1 -1
  64. package/tests/e2e/issue-160.e2e.test.js +291 -0
  65. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +1 -1
  66. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +1 -1
  67. package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +2 -2
  68. package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +4 -0
  69. package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +1 -1
  70. package/tests/fixtures/traces/conformance/expected/trace.lino +2 -2
  71. package/tests/unit/browser/issue-160-connector.test.js +157 -0
  72. package/tests/unit/browser/launch-building-blocks.test.js +5 -1
  73. package/tests/unit/browser/launcher.test.js +5 -1
  74. package/tests/unit/browser/persistent-session.test.js +59 -0
  75. package/tests/unit/browser/quiet-ui.test.js +16 -0
  76. package/tests/unit/browser/tab-selection.test.js +38 -0
  77. package/tests/unit/capture/capture.test.js +52 -0
  78. package/tests/unit/capture/cleanup.test.js +24 -0
  79. package/tests/unit/capture/viewport.test.js +168 -0
  80. package/tests/unit/cli/dispatcher.test.js +25 -0
  81. package/tests/unit/core/interrupted-navigation.test.js +32 -0
  82. package/tests/unit/core/readiness.test.js +21 -0
  83. package/tests/unit/core/trigger-readiness.test.js +39 -0
  84. package/tests/unit/core/trigger-stop-race.test.js +80 -0
  85. package/tests/unit/elements/visible-count.test.js +24 -0
  86. package/tests/unit/elements/wait-navigation.test.js +26 -0
  87. package/tests/unit/interactions/navigation-timeout.test.js +39 -0
  88. package/tests/unit/interactions/overlays.test.js +58 -0
  89. package/tests/unit/scripts/ci-timeout-budgets.test.js +1 -0
  90. package/tests/unit/traces/bundle.test.js +16 -0
  91. package/tests/unit/traces/issue-160-privacy.test.js +110 -0
  92. package/tests/unit/traces/issue-160-rotation.test.js +140 -0
  93. package/tests/unit/traces/network.test.js +149 -0
@@ -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
+ });
@@ -73,7 +73,11 @@ describe('launch restrictions', () => {
73
73
  GOOGLE_DEFAULT_CLIENT_ID: 'no',
74
74
  GOOGLE_DEFAULT_CLIENT_SECRET: 'no',
75
75
  });
76
- assert.ok(resolved.args.includes('--disable-features=Translate'));
76
+ assert.ok(
77
+ resolved.args.includes(
78
+ '--disable-features=SessionRestoreInfobar,Translate'
79
+ )
80
+ );
77
81
  assert.ok(LAUNCH_RESTRICTION_PRESETS['legacy-launch-browser']);
78
82
  });
79
83
 
@@ -202,7 +202,11 @@ describe('launchBrowser', () => {
202
202
  assert.equal(options.env.GOOGLE_API_KEY, 'no');
203
203
  assert.equal(process.env.GOOGLE_API_KEY, googleBefore);
204
204
  assert.ok(options.args.includes('--no-first-run'));
205
- assert.ok(options.args.includes('--disable-features=Translate'));
205
+ assert.ok(
206
+ options.args.includes(
207
+ '--disable-features=SessionRestoreInfobar,Translate'
208
+ )
209
+ );
206
210
  await result.close();
207
211
  });
208
212
 
@@ -0,0 +1,59 @@
1
+ import { it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { closeRemoteBrowser } from '../../../src/browser/persistent-session.js';
4
+
5
+ function websocket(context) {
6
+ const sockets = [];
7
+ class Socket {
8
+ constructor() {
9
+ sockets.push(this);
10
+ globalThis.queueMicrotask(() => this.onopen());
11
+ }
12
+ send(message) {
13
+ this.request = JSON.parse(message);
14
+ }
15
+ close() {
16
+ this.clientClosed = true;
17
+ this.onclose();
18
+ }
19
+ message(value) {
20
+ this.onmessage({ data: JSON.stringify(value) });
21
+ }
22
+ }
23
+ function createSocket() {
24
+ return new Socket();
25
+ }
26
+ context.mock.method(globalThis, 'WebSocket', createSocket);
27
+ return sockets;
28
+ }
29
+
30
+ it('waits for browser shutdown rather than closing on unrelated CDP events', async (context) => {
31
+ const sockets = websocket(context);
32
+ const closing = closeRemoteBrowser('ws://127.0.0.1/browser');
33
+ const socket = sockets[0];
34
+ await Promise.resolve();
35
+ socket.message({ method: 'Target.targetCreated', params: {} });
36
+ const prematurelyClosed = socket.clientClosed;
37
+ socket.message({ id: socket.request.id, result: {} });
38
+ socket.onclose();
39
+ await closing;
40
+ assert.equal(prematurelyClosed, undefined);
41
+ });
42
+
43
+ it('reports CDP shutdown errors instead of claiming the browser closed', async (context) => {
44
+ const sockets = websocket(context);
45
+ const closing = closeRemoteBrowser('ws://127.0.0.1/browser');
46
+ await Promise.resolve();
47
+ sockets[0].message({ id: 1, error: { message: 'shutdown refused' } });
48
+ await assert.rejects(closing, /shutdown refused/);
49
+ });
50
+
51
+ it('accepts Chrome disconnecting abruptly after its shutdown acknowledgement', async (context) => {
52
+ const sockets = websocket(context);
53
+ const closing = closeRemoteBrowser('ws://127.0.0.1/browser');
54
+ await Promise.resolve();
55
+ sockets[0].message({ id: 1, result: {} });
56
+ sockets[0].onerror();
57
+ sockets[0].onclose();
58
+ await closing;
59
+ });
@@ -0,0 +1,16 @@
1
+ import { it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { resolveRestrictions } from '../../../src/browser/restrictions.js';
4
+
5
+ it('quiet-ui merges caller features and disables Translate in preferences', () => {
6
+ const result = resolveRestrictions(['quiet-ui'], {
7
+ disableFeatures: ['CustomFeature', 'Translate'],
8
+ });
9
+ const switches = result.args.filter((arg) =>
10
+ arg.startsWith('--disable-features=')
11
+ );
12
+ assert.equal(switches.length, 1);
13
+ assert.ok(switches[0].includes('SessionRestoreInfobar'));
14
+ assert.ok(switches[0].includes('CustomFeature'));
15
+ assert.equal(result.preferences.translate.enabled, false);
16
+ });
@@ -0,0 +1,38 @@
1
+ import { it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { pickForegroundPage } from '../../../src/browser/connector.js';
4
+
5
+ it('explicit URL selection wins over emulated visibility', async () => {
6
+ const pages = ['background', 'automation'].map((name) => ({
7
+ url: () => `https://example.com/${name}`,
8
+ evaluate: async () => 'visible',
9
+ }));
10
+ assert.equal(
11
+ await pickForegroundPage(pages, { url: /automation/ }),
12
+ pages[1]
13
+ );
14
+ await assert.rejects(pickForegroundPage(pages, { url: /missing/ }), /No tab/);
15
+ });
16
+
17
+ it('focus emulation is disabled before reading foreground visibility', async () => {
18
+ let emulated = true;
19
+ const session = {
20
+ send: async (method) => {
21
+ if (method === 'Emulation.setFocusEmulationEnabled') {
22
+ emulated = false;
23
+ }
24
+ return { targetInfo: { targetId: 'background' } };
25
+ },
26
+ detach: async () => {},
27
+ };
28
+ const context = { newCDPSession: async () => session };
29
+ const background = {
30
+ context: () => context,
31
+ evaluate: async () => (emulated ? 'visible' : 'hidden'),
32
+ };
33
+ const foreground = {
34
+ context: () => context,
35
+ evaluate: async () => 'visible',
36
+ };
37
+ assert.equal(await pickForegroundPage([background, foreground]), foreground);
38
+ });
@@ -0,0 +1,52 @@
1
+ import { it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import UPNG from 'upng-js';
4
+ import {
5
+ screenshot,
6
+ encodeAnimation,
7
+ UnsupportedCaptureError,
8
+ } from '../../../src/capture/index.js';
9
+
10
+ const png = (color) =>
11
+ Buffer.from(
12
+ UPNG.encode([new Uint8Array(color).buffer], 1, 1, 0, undefined, true)
13
+ );
14
+ it('screenshots honor WebP format without handing unsupported options to the engine', async () => {
15
+ const page = {
16
+ screenshot: async (options) => {
17
+ assert.equal(options.type, 'png');
18
+ return png([255, 0, 0, 255]);
19
+ },
20
+ };
21
+ const result = await screenshot({
22
+ page,
23
+ engine: 'playwright',
24
+ format: 'webp',
25
+ quality: 80,
26
+ });
27
+ assert.equal(result.subarray(8, 12).toString(), 'WEBP');
28
+ });
29
+ it('animation encoders produce multiple frames with no external binary', async () => {
30
+ const frames = [png([255, 0, 0, 255]), png([0, 0, 255, 255])];
31
+ assert.equal(
32
+ (await encodeAnimation(frames, { format: 'gif', fps: 5 }))
33
+ .subarray(0, 6)
34
+ .toString(),
35
+ 'GIF89a'
36
+ );
37
+ const apng = await encodeAnimation(frames, { format: 'apng', fps: 5 });
38
+ assert.equal(
39
+ UPNG.decode(
40
+ apng.buffer.slice(apng.byteOffset, apng.byteOffset + apng.length)
41
+ ).frames.length,
42
+ 2
43
+ );
44
+ const webp = await encodeAnimation(frames, { format: 'webp', fps: 5 });
45
+ assert.equal(webp.toString('latin1').split('ANMF').length - 1, 2);
46
+ });
47
+ it('rejects unsupported options with a typed capability error', async () => {
48
+ await assert.rejects(
49
+ screenshot({ page: {}, engine: 'unknown' }),
50
+ UnsupportedCaptureError
51
+ );
52
+ });
@@ -0,0 +1,24 @@
1
+ import { it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createTraceStop } from '../../../src/traces/stop.js';
4
+
5
+ it('failed video export still releases the trace resources', async () => {
6
+ const released = [];
7
+ const stop = createTraceStop({
8
+ getStopped: () => false,
9
+ setStopped: () => {},
10
+ options: {},
11
+ detachers: [],
12
+ navigationCapture: async () => {},
13
+ video: {
14
+ stop: async () => {
15
+ throw new Error('encoder failed');
16
+ },
17
+ },
18
+ detachNetwork: async () => released.push('network'),
19
+ mutations: { stop: async () => released.push('mutations') },
20
+ bundle: { abort: async () => released.push('bundle') },
21
+ });
22
+ await assert.rejects(stop(), /encoder failed/);
23
+ assert.deepEqual(released, ['network', 'mutations', 'bundle']);
24
+ });