@monoes/monobrowse 1.0.11 → 1.0.13
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/dist/src/__tests__/adapters.test.js +5 -2
- package/dist/src/__tests__/adapters.test.js.map +1 -1
- package/dist/src/__tests__/batch-eval-parsing.test.js +4 -2
- package/dist/src/__tests__/batch-eval-parsing.test.js.map +1 -1
- package/dist/src/__tests__/box-model.test.js +9 -5
- package/dist/src/__tests__/box-model.test.js.map +1 -1
- package/dist/src/__tests__/browser-launch.test.d.ts +10 -0
- package/dist/src/__tests__/browser-launch.test.d.ts.map +1 -1
- package/dist/src/__tests__/browser-launch.test.js +17 -10
- package/dist/src/__tests__/browser-launch.test.js.map +1 -1
- package/dist/src/__tests__/cdp-client.test.js +9 -3
- package/dist/src/__tests__/cdp-client.test.js.map +1 -1
- package/dist/src/__tests__/close-browser.test.d.ts +13 -0
- package/dist/src/__tests__/close-browser.test.d.ts.map +1 -1
- package/dist/src/__tests__/close-browser.test.js +5 -5
- package/dist/src/__tests__/close-browser.test.js.map +1 -1
- package/dist/src/__tests__/console-log.test.js +17 -6
- package/dist/src/__tests__/console-log.test.js.map +1 -1
- package/dist/src/__tests__/find.test.js +26 -6
- package/dist/src/__tests__/find.test.js.map +1 -1
- package/dist/src/__tests__/frame-command.test.js +9 -3
- package/dist/src/__tests__/frame-command.test.js.map +1 -1
- package/dist/src/__tests__/open-url-navigate-error.test.js +1 -3
- package/dist/src/__tests__/open-url-navigate-error.test.js.map +1 -1
- package/dist/src/__tests__/output.test.js +1 -1
- package/dist/src/__tests__/output.test.js.map +1 -1
- package/dist/src/__tests__/profiler-heap.test.d.ts +2 -0
- package/dist/src/__tests__/profiler-heap.test.d.ts.map +1 -0
- package/dist/src/__tests__/profiler-heap.test.js +154 -0
- package/dist/src/__tests__/profiler-heap.test.js.map +1 -0
- package/dist/src/__tests__/reap-idle-browser.test.d.ts +15 -0
- package/dist/src/__tests__/reap-idle-browser.test.d.ts.map +1 -0
- package/dist/src/__tests__/reap-idle-browser.test.js +238 -0
- package/dist/src/__tests__/reap-idle-browser.test.js.map +1 -0
- package/dist/src/__tests__/ref-cache.test.js +18 -10
- package/dist/src/__tests__/ref-cache.test.js.map +1 -1
- package/dist/src/__tests__/report-a11y.test.d.ts +2 -0
- package/dist/src/__tests__/report-a11y.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-a11y.test.js +147 -0
- package/dist/src/__tests__/report-a11y.test.js.map +1 -0
- package/dist/src/__tests__/report-analyze.test.d.ts +2 -0
- package/dist/src/__tests__/report-analyze.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-analyze.test.js +157 -0
- package/dist/src/__tests__/report-analyze.test.js.map +1 -0
- package/dist/src/__tests__/report-budget.test.d.ts +2 -0
- package/dist/src/__tests__/report-budget.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-budget.test.js +80 -0
- package/dist/src/__tests__/report-budget.test.js.map +1 -0
- package/dist/src/__tests__/report-diff.test.d.ts +2 -0
- package/dist/src/__tests__/report-diff.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-diff.test.js +231 -0
- package/dist/src/__tests__/report-diff.test.js.map +1 -0
- package/dist/src/__tests__/report-evidence.test.d.ts +2 -0
- package/dist/src/__tests__/report-evidence.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-evidence.test.js +214 -0
- package/dist/src/__tests__/report-evidence.test.js.map +1 -0
- package/dist/src/__tests__/report-flake.test.d.ts +2 -0
- package/dist/src/__tests__/report-flake.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-flake.test.js +182 -0
- package/dist/src/__tests__/report-flake.test.js.map +1 -0
- package/dist/src/__tests__/report-history.test.d.ts +2 -0
- package/dist/src/__tests__/report-history.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-history.test.js +229 -0
- package/dist/src/__tests__/report-history.test.js.map +1 -0
- package/dist/src/__tests__/report-output.test.d.ts +2 -0
- package/dist/src/__tests__/report-output.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-output.test.js +169 -0
- package/dist/src/__tests__/report-output.test.js.map +1 -0
- package/dist/src/__tests__/report-png.test.d.ts +2 -0
- package/dist/src/__tests__/report-png.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-png.test.js +84 -0
- package/dist/src/__tests__/report-png.test.js.map +1 -0
- package/dist/src/__tests__/report-render-extras.test.d.ts +2 -0
- package/dist/src/__tests__/report-render-extras.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-render-extras.test.js +285 -0
- package/dist/src/__tests__/report-render-extras.test.js.map +1 -0
- package/dist/src/__tests__/report-render.test.d.ts +2 -0
- package/dist/src/__tests__/report-render.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-render.test.js +143 -0
- package/dist/src/__tests__/report-render.test.js.map +1 -0
- package/dist/src/__tests__/report-run.test.d.ts +2 -0
- package/dist/src/__tests__/report-run.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-run.test.js +234 -0
- package/dist/src/__tests__/report-run.test.js.map +1 -0
- package/dist/src/__tests__/session-state.test.d.ts +8 -0
- package/dist/src/__tests__/session-state.test.d.ts.map +1 -1
- package/dist/src/__tests__/session-state.test.js +10 -10
- package/dist/src/__tests__/session-state.test.js.map +1 -1
- package/dist/src/__tests__/snapshot.test.js +44 -14
- package/dist/src/__tests__/snapshot.test.js.map +1 -1
- package/dist/src/__tests__/storage-eval.test.js +14 -6
- package/dist/src/__tests__/storage-eval.test.js.map +1 -1
- package/dist/src/__tests__/tabs.test.js +25 -9
- package/dist/src/__tests__/tabs.test.js.map +1 -1
- package/dist/src/browser/action-builder/analyzer.d.ts +1 -1
- package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -1
- package/dist/src/browser/action-builder/analyzer.js +25 -9
- package/dist/src/browser/action-builder/analyzer.js.map +1 -1
- package/dist/src/browser/action-builder/types.d.ts.map +1 -1
- package/dist/src/browser/actions.d.ts +1 -1
- package/dist/src/browser/actions.d.ts.map +1 -1
- package/dist/src/browser/actions.js +16 -6
- package/dist/src/browser/actions.js.map +1 -1
- package/dist/src/browser/adapters/gemini.d.ts.map +1 -1
- package/dist/src/browser/adapters/gemini.js.map +1 -1
- package/dist/src/browser/adapters/google.d.ts.map +1 -1
- package/dist/src/browser/adapters/google.js.map +1 -1
- package/dist/src/browser/adapters/index.d.ts.map +1 -1
- package/dist/src/browser/adapters/index.js +3 -3
- package/dist/src/browser/adapters/index.js.map +1 -1
- package/dist/src/browser/adapters/instagram.d.ts.map +1 -1
- package/dist/src/browser/adapters/instagram.js.map +1 -1
- package/dist/src/browser/adapters/linkedin.d.ts.map +1 -1
- package/dist/src/browser/adapters/linkedin.js.map +1 -1
- package/dist/src/browser/adapters/microsoft.d.ts.map +1 -1
- package/dist/src/browser/adapters/microsoft.js.map +1 -1
- package/dist/src/browser/adapters/x.d.ts.map +1 -1
- package/dist/src/browser/adapters/x.js.map +1 -1
- package/dist/src/browser/browser.d.ts +30 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +122 -15
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +21 -8
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/console-log.d.ts.map +1 -1
- package/dist/src/browser/console-log.js +2 -2
- package/dist/src/browser/console-log.js.map +1 -1
- package/dist/src/browser/dashboard/server.d.ts.map +1 -1
- package/dist/src/browser/dashboard/server.js +29 -19
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/browser/dashboard/ui.html +153 -165
- package/dist/src/browser/dialog.d.ts.map +1 -1
- package/dist/src/browser/dialog.js +3 -1
- package/dist/src/browser/dialog.js.map +1 -1
- package/dist/src/browser/emulation.d.ts.map +1 -1
- package/dist/src/browser/emulation.js +21 -6
- package/dist/src/browser/emulation.js.map +1 -1
- package/dist/src/browser/find.d.ts +4 -4
- package/dist/src/browser/find.d.ts.map +1 -1
- package/dist/src/browser/find.js +14 -6
- package/dist/src/browser/find.js.map +1 -1
- package/dist/src/browser/har.d.ts.map +1 -1
- package/dist/src/browser/har.js +14 -7
- package/dist/src/browser/har.js.map +1 -1
- package/dist/src/browser/index.d.ts +17 -16
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +17 -16
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/network.d.ts.map +1 -1
- package/dist/src/browser/network.js +14 -4
- package/dist/src/browser/network.js.map +1 -1
- package/dist/src/browser/pdf.d.ts.map +1 -1
- package/dist/src/browser/pdf.js +3 -3
- package/dist/src/browser/pdf.js.map +1 -1
- package/dist/src/browser/profiler.d.ts.map +1 -1
- package/dist/src/browser/profiler.js +22 -20
- package/dist/src/browser/profiler.js.map +1 -1
- package/dist/src/browser/record.d.ts +1 -1
- package/dist/src/browser/record.d.ts.map +1 -1
- package/dist/src/browser/record.js +18 -7
- package/dist/src/browser/record.js.map +1 -1
- package/dist/src/browser/ref-cache.d.ts.map +1 -1
- package/dist/src/browser/ref-cache.js +9 -4
- package/dist/src/browser/ref-cache.js.map +1 -1
- package/dist/src/browser/screenshot.d.ts.map +1 -1
- package/dist/src/browser/screenshot.js +11 -7
- package/dist/src/browser/screenshot.js.map +1 -1
- package/dist/src/browser/session.d.ts +1 -1
- package/dist/src/browser/session.d.ts.map +1 -1
- package/dist/src/browser/session.js +27 -8
- package/dist/src/browser/session.js.map +1 -1
- package/dist/src/browser/snapshot.d.ts +1 -1
- package/dist/src/browser/snapshot.d.ts.map +1 -1
- package/dist/src/browser/snapshot.js +8 -4
- package/dist/src/browser/snapshot.js.map +1 -1
- package/dist/src/browser/storage.d.ts.map +1 -1
- package/dist/src/browser/storage.js.map +1 -1
- package/dist/src/browser/tabs.d.ts.map +1 -1
- package/dist/src/browser/tabs.js +9 -3
- package/dist/src/browser/tabs.js.map +1 -1
- package/dist/src/browser/trace.d.ts.map +1 -1
- package/dist/src/browser/trace.js +3 -3
- package/dist/src/browser/trace.js.map +1 -1
- package/dist/src/browser/types.d.ts.map +1 -1
- package/dist/src/browser/types.js +23 -4
- package/dist/src/browser/types.js.map +1 -1
- package/dist/src/browser/vitals.d.ts.map +1 -1
- package/dist/src/browser/vitals.js +1 -1
- package/dist/src/browser/vitals.js.map +1 -1
- package/dist/src/browser/wait.d.ts.map +1 -1
- package/dist/src/browser/wait.js +11 -5
- package/dist/src/browser/wait.js.map +1 -1
- package/dist/src/cli/action.d.ts.map +1 -1
- package/dist/src/cli/action.js +14 -11
- package/dist/src/cli/action.js.map +1 -1
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +707 -159
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/output.js +5 -5
- package/dist/src/cli/output.js.map +1 -1
- package/dist/src/cli/platform.d.ts.map +1 -1
- package/dist/src/cli/platform.js +15 -11
- package/dist/src/cli/platform.js.map +1 -1
- package/dist/src/cli/types.d.ts +1 -1
- package/dist/src/cli/types.d.ts.map +1 -1
- package/dist/src/cli.js +21 -9
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/report/a11y.d.ts +52 -0
- package/dist/src/report/a11y.d.ts.map +1 -0
- package/dist/src/report/a11y.js +231 -0
- package/dist/src/report/a11y.js.map +1 -0
- package/dist/src/report/analyze.d.ts +20 -0
- package/dist/src/report/analyze.d.ts.map +1 -0
- package/dist/src/report/analyze.js +136 -0
- package/dist/src/report/analyze.js.map +1 -0
- package/dist/src/report/budget.d.ts +27 -0
- package/dist/src/report/budget.d.ts.map +1 -0
- package/dist/src/report/budget.js +143 -0
- package/dist/src/report/budget.js.map +1 -0
- package/dist/src/report/collect-a11y.d.ts +25 -0
- package/dist/src/report/collect-a11y.d.ts.map +1 -0
- package/dist/src/report/collect-a11y.js +154 -0
- package/dist/src/report/collect-a11y.js.map +1 -0
- package/dist/src/report/collect.d.ts +45 -0
- package/dist/src/report/collect.d.ts.map +1 -0
- package/dist/src/report/collect.js +275 -0
- package/dist/src/report/collect.js.map +1 -0
- package/dist/src/report/evidence.d.ts +80 -0
- package/dist/src/report/evidence.d.ts.map +1 -0
- package/dist/src/report/evidence.js +279 -0
- package/dist/src/report/evidence.js.map +1 -0
- package/dist/src/report/flake.d.ts +88 -0
- package/dist/src/report/flake.d.ts.map +1 -0
- package/dist/src/report/flake.js +232 -0
- package/dist/src/report/flake.js.map +1 -0
- package/dist/src/report/history.d.ts +85 -0
- package/dist/src/report/history.d.ts.map +1 -0
- package/dist/src/report/history.js +176 -0
- package/dist/src/report/history.js.map +1 -0
- package/dist/src/report/index.d.ts +102 -0
- package/dist/src/report/index.d.ts.map +1 -0
- package/dist/src/report/index.js +216 -0
- package/dist/src/report/index.js.map +1 -0
- package/dist/src/report/pixel-diff.d.ts +30 -0
- package/dist/src/report/pixel-diff.d.ts.map +1 -0
- package/dist/src/report/pixel-diff.js +175 -0
- package/dist/src/report/pixel-diff.js.map +1 -0
- package/dist/src/report/png.d.ts +28 -0
- package/dist/src/report/png.d.ts.map +1 -0
- package/dist/src/report/png.js +259 -0
- package/dist/src/report/png.js.map +1 -0
- package/dist/src/report/render-evidence.d.ts +16 -0
- package/dist/src/report/render-evidence.d.ts.map +1 -0
- package/dist/src/report/render-evidence.js +174 -0
- package/dist/src/report/render-evidence.js.map +1 -0
- package/dist/src/report/render-trend.d.ts +16 -0
- package/dist/src/report/render-trend.d.ts.map +1 -0
- package/dist/src/report/render-trend.js +194 -0
- package/dist/src/report/render-trend.js.map +1 -0
- package/dist/src/report/render.d.ts +21 -0
- package/dist/src/report/render.d.ts.map +1 -0
- package/dist/src/report/render.js +286 -0
- package/dist/src/report/render.js.map +1 -0
- package/dist/src/report/run-diff.d.ts +18 -0
- package/dist/src/report/run-diff.d.ts.map +1 -0
- package/dist/src/report/run-diff.js +51 -0
- package/dist/src/report/run-diff.js.map +1 -0
- package/dist/src/report/structure.d.ts +41 -0
- package/dist/src/report/structure.d.ts.map +1 -0
- package/dist/src/report/structure.js +210 -0
- package/dist/src/report/structure.js.map +1 -0
- package/dist/src/report/trend.d.ts +26 -0
- package/dist/src/report/trend.d.ts.map +1 -0
- package/dist/src/report/trend.js +164 -0
- package/dist/src/report/trend.js.map +1 -0
- package/dist/src/report/types.d.ts +320 -0
- package/dist/src/report/types.d.ts.map +1 -0
- package/dist/src/report/types.js +11 -0
- package/dist/src/report/types.js.map +1 -0
- package/dist/src/report/util.d.ts +17 -0
- package/dist/src/report/util.d.ts.map +1 -0
- package/dist/src/report/util.js +45 -0
- package/dist/src/report/util.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/__tests__/adapters.test.ts +15 -8
- package/src/__tests__/batch-eval-parsing.test.ts +4 -2
- package/src/__tests__/box-model.test.ts +9 -5
- package/src/__tests__/browser-launch.test.ts +26 -15
- package/src/__tests__/cdp-client.test.ts +25 -9
- package/src/__tests__/close-browser.test.ts +11 -6
- package/src/__tests__/console-log.test.ts +36 -13
- package/src/__tests__/find.test.ts +58 -20
- package/src/__tests__/frame-command.test.ts +12 -6
- package/src/__tests__/open-url-navigate-error.test.ts +2 -4
- package/src/__tests__/output.test.ts +1 -1
- package/src/__tests__/profiler-heap.test.ts +191 -0
- package/src/__tests__/reap-idle-browser.test.ts +276 -0
- package/src/__tests__/ref-cache.test.ts +25 -12
- package/src/__tests__/report-a11y.test.ts +180 -0
- package/src/__tests__/report-analyze.test.ts +202 -0
- package/src/__tests__/report-budget.test.ts +92 -0
- package/src/__tests__/report-diff.test.ts +272 -0
- package/src/__tests__/report-evidence.test.ts +276 -0
- package/src/__tests__/report-flake.test.ts +214 -0
- package/src/__tests__/report-history.test.ts +293 -0
- package/src/__tests__/report-output.test.ts +210 -0
- package/src/__tests__/report-png.test.ts +95 -0
- package/src/__tests__/report-render-extras.test.ts +315 -0
- package/src/__tests__/report-render.test.ts +175 -0
- package/src/__tests__/report-run.test.ts +264 -0
- package/src/__tests__/session-state.test.ts +40 -28
- package/src/__tests__/snapshot.test.ts +60 -21
- package/src/__tests__/storage-eval.test.ts +30 -16
- package/src/__tests__/tabs.test.ts +26 -10
- package/src/browser/action-builder/analyzer.ts +39 -15
- package/src/browser/action-builder/types.ts +45 -8
- package/src/browser/actions.ts +288 -123
- package/src/browser/adapters/gemini.ts +7 -3
- package/src/browser/adapters/google.ts +5 -3
- package/src/browser/adapters/index.ts +5 -4
- package/src/browser/adapters/instagram.ts +4 -2
- package/src/browser/adapters/linkedin.ts +4 -2
- package/src/browser/adapters/microsoft.ts +9 -5
- package/src/browser/adapters/x.ts +7 -3
- package/src/browser/browser.ts +188 -42
- package/src/browser/cdp.ts +43 -11
- package/src/browser/console-log.ts +24 -7
- package/src/browser/dashboard/server.ts +44 -25
- package/src/browser/dashboard/ui.html +153 -165
- package/src/browser/dialog.ts +25 -8
- package/src/browser/emulation.ts +86 -36
- package/src/browser/find.ts +110 -47
- package/src/browser/har.ts +58 -26
- package/src/browser/index.ts +17 -16
- package/src/browser/network.ts +147 -46
- package/src/browser/pdf.ts +4 -4
- package/src/browser/profiler.ts +39 -19
- package/src/browser/record.ts +38 -19
- package/src/browser/ref-cache.ts +38 -17
- package/src/browser/screenshot.ts +58 -32
- package/src/browser/session.ts +51 -18
- package/src/browser/snapshot.ts +34 -20
- package/src/browser/storage.ts +73 -17
- package/src/browser/tabs.ts +29 -17
- package/src/browser/trace.ts +14 -10
- package/src/browser/types.ts +23 -4
- package/src/browser/vitals.ts +33 -16
- package/src/browser/wait.ts +47 -21
- package/src/cli/action.ts +52 -21
- package/src/cli/commands.ts +1365 -385
- package/src/cli/output.ts +5 -5
- package/src/cli/platform.ts +44 -23
- package/src/cli/types.ts +1 -1
- package/src/cli.ts +34 -18
- package/src/index.ts +2 -2
- package/src/report/a11y.ts +242 -0
- package/src/report/analyze.ts +181 -0
- package/src/report/budget.ts +155 -0
- package/src/report/collect-a11y.ts +200 -0
- package/src/report/collect.ts +408 -0
- package/src/report/evidence.ts +379 -0
- package/src/report/flake.ts +303 -0
- package/src/report/history.ts +219 -0
- package/src/report/index.ts +307 -0
- package/src/report/pixel-diff.ts +197 -0
- package/src/report/png.ts +290 -0
- package/src/report/render-evidence.ts +194 -0
- package/src/report/render-trend.ts +225 -0
- package/src/report/render.ts +314 -0
- package/src/report/run-diff.ts +63 -0
- package/src/report/structure.ts +212 -0
- package/src/report/trend.ts +189 -0
- package/src/report/types.ts +356 -0
- package/src/report/util.ts +48 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renderer's contract is that the HTML is SELF-CONTAINED: it must render
|
|
3
|
+
* identically on a machine with no network, because monomind's document
|
|
4
|
+
* pipeline ingests it as an ordinary document and because an archived report
|
|
5
|
+
* has to still work years later. These tests enforce that mechanically.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
import { buildReport } from '../report/analyze.js';
|
|
9
|
+
import { DEFAULT_BUDGET, parseBudget } from '../report/budget.js';
|
|
10
|
+
import { escapeHtml, renderHtml } from '../report/render.js';
|
|
11
|
+
import type { CaptureData, Report } from '../report/types.js';
|
|
12
|
+
|
|
13
|
+
const PNG =
|
|
14
|
+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
|
|
15
|
+
|
|
16
|
+
function capture(partial: Partial<CaptureData> = {}): CaptureData {
|
|
17
|
+
return {
|
|
18
|
+
url: 'https://shop.test/checkout',
|
|
19
|
+
finalUrl: 'https://shop.test/checkout',
|
|
20
|
+
title: 'Checkout',
|
|
21
|
+
capturedAt: '2026-09-21T10:00:00.000Z',
|
|
22
|
+
durationMs: 3200,
|
|
23
|
+
console: [],
|
|
24
|
+
pageErrors: [],
|
|
25
|
+
requests: [],
|
|
26
|
+
vitals: {},
|
|
27
|
+
a11y: [],
|
|
28
|
+
screenshots: [],
|
|
29
|
+
notes: [],
|
|
30
|
+
...partial,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function report(partial: Partial<CaptureData> = {}, budget = DEFAULT_BUDGET): Report {
|
|
35
|
+
return buildReport(capture(partial), budget);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Anything that would make the browser go to the network to render the page. */
|
|
39
|
+
function externalReferences(html: string): string[] {
|
|
40
|
+
const patterns = [
|
|
41
|
+
/<script[^>]+\ssrc\s*=/gi,
|
|
42
|
+
/<link\b/gi,
|
|
43
|
+
/@import/gi,
|
|
44
|
+
/url\(\s*['"]?https?:/gi,
|
|
45
|
+
/\s(?:src|href|poster|action|data)\s*=\s*["']https?:/gi,
|
|
46
|
+
/\s(?:src|href)\s*=\s*["']\/\//gi,
|
|
47
|
+
];
|
|
48
|
+
return patterns.flatMap((p) => html.match(p) ?? []);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('renderHtml — self-containment', () => {
|
|
52
|
+
it('references no external resource of any kind', () => {
|
|
53
|
+
const html = renderHtml(
|
|
54
|
+
report({
|
|
55
|
+
screenshots: [{ label: 'page', width: 1280, height: 3000, dataUrl: PNG }],
|
|
56
|
+
requests: [{ url: 'https://cdn.test/app.js', method: 'GET', status: 404, failed: true }],
|
|
57
|
+
console: [{ type: 'error', text: 'boom', url: 'https://cdn.test/app.js', timestamp: 0 }],
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
expect(externalReferences(html)).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('inlines its CSS in a style element and inlines screenshots as data URIs', () => {
|
|
64
|
+
const html = renderHtml(
|
|
65
|
+
report({ screenshots: [{ label: 'page', width: 800, height: 600, dataUrl: PNG }] }),
|
|
66
|
+
);
|
|
67
|
+
expect(html).toContain('<style>');
|
|
68
|
+
expect(html).toContain('src="data:image/png;base64,');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('themes for both light and dark without JavaScript', () => {
|
|
72
|
+
const html = renderHtml(report());
|
|
73
|
+
expect(html).toContain('@media (prefers-color-scheme: dark)');
|
|
74
|
+
expect(html).not.toMatch(/<script(?![^>]*type="application\/json")/i);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('renderHtml — content', () => {
|
|
79
|
+
it('leads with a PASS verdict for a clean page', () => {
|
|
80
|
+
const html = renderHtml(report({ vitals: { lcp: 800, cls: 0.01, inp: 20 } }));
|
|
81
|
+
const verdictAt = html.indexOf('PASS');
|
|
82
|
+
expect(verdictAt).toBeGreaterThan(-1);
|
|
83
|
+
expect(verdictAt).toBeLessThan(html.indexOf('Failed requests'));
|
|
84
|
+
expect(html).toContain('All budgets met.');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('orders the sections verdict, errors, failed requests, vitals, a11y, screenshots', () => {
|
|
88
|
+
const html = renderHtml(report());
|
|
89
|
+
const order = [
|
|
90
|
+
'PASS',
|
|
91
|
+
'Errors',
|
|
92
|
+
'Failed requests',
|
|
93
|
+
'Web vitals',
|
|
94
|
+
'Accessibility',
|
|
95
|
+
'Screenshots',
|
|
96
|
+
];
|
|
97
|
+
const positions = order.map((s) => html.indexOf(s));
|
|
98
|
+
expect(positions.every((p) => p > -1)).toBe(true);
|
|
99
|
+
expect([...positions].sort((a, b) => a - b)).toEqual(positions);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('shows each budget failure with its expected and actual values', () => {
|
|
103
|
+
const html = renderHtml(
|
|
104
|
+
report({ vitals: { lcp: 4100, cls: 0, inp: 0 } }, parseBudget({ lcp: 2500 })),
|
|
105
|
+
);
|
|
106
|
+
expect(html).toContain('FAIL');
|
|
107
|
+
expect(html).toContain('LCP (Largest Contentful Paint)');
|
|
108
|
+
expect(html).toContain('<= 2500ms');
|
|
109
|
+
expect(html).toContain('4100ms');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('lists failed requests and omits the successful ones', () => {
|
|
113
|
+
const html = renderHtml(
|
|
114
|
+
report({
|
|
115
|
+
requests: [
|
|
116
|
+
{ url: 'https://shop.test/api/cart', method: 'POST', status: 500, failed: true },
|
|
117
|
+
{ url: 'https://shop.test/ok', method: 'GET', status: 200, failed: false },
|
|
118
|
+
],
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
expect(html).toContain('https://shop.test/api/cart');
|
|
122
|
+
expect(html).not.toContain('https://shop.test/ok');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('says a metric was not measured rather than showing a fake zero', () => {
|
|
126
|
+
const html = renderHtml(report({ vitals: { cls: 0.05 } }));
|
|
127
|
+
expect(html).toContain('not measured');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('renders a11y findings with role, name state and locator', () => {
|
|
131
|
+
const html = renderHtml(
|
|
132
|
+
report({
|
|
133
|
+
a11y: [
|
|
134
|
+
{
|
|
135
|
+
rule: 'unlabelled-control',
|
|
136
|
+
impact: 'error',
|
|
137
|
+
role: 'button',
|
|
138
|
+
name: null,
|
|
139
|
+
locator: 'form > button:nth-of-type(2)',
|
|
140
|
+
detail: 'button has no accessible name',
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
expect(html).toContain('unlabelled-control');
|
|
146
|
+
expect(html).toContain('form > button:nth-of-type(2)');
|
|
147
|
+
expect(html).toContain('(no accessible name)');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('states plainly that contrast was not evaluated', () => {
|
|
151
|
+
expect(renderHtml(report())).toMatch(/[Cc]olour contrast is not evaluated/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('surfaces degraded-run notes', () => {
|
|
155
|
+
const html = renderHtml(report({ notes: ['Page never went network-idle within 20000ms'] }));
|
|
156
|
+
expect(html).toContain('Run notes');
|
|
157
|
+
expect(html).toContain('never went network-idle');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('escapeHtml', () => {
|
|
162
|
+
it('neutralizes markup from page-controlled strings', () => {
|
|
163
|
+
expect(escapeHtml('<img src=x onerror="alert(1)">')).toBe(
|
|
164
|
+
'<img src=x onerror="alert(1)">',
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('keeps an injected error message from breaking out of the document', () => {
|
|
169
|
+
const html = renderHtml(
|
|
170
|
+
report({ pageErrors: [{ text: '</table><script>alert(1)</script>', timestamp: 0 }] }),
|
|
171
|
+
);
|
|
172
|
+
expect(html).not.toContain('<script>alert(1)</script>');
|
|
173
|
+
expect(html).toContain('<script>');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The orchestration seam in report/index.ts: history is read before the run
|
|
3
|
+
* is saved, evidence is only built when it is warranted, and `--repeat`
|
|
4
|
+
* judges the set rather than the last run.
|
|
5
|
+
*
|
|
6
|
+
* `collect` is mocked, so nothing here launches Chrome. Output and history
|
|
7
|
+
* both go to a scratch directory under the monomind data dir — never the
|
|
8
|
+
* shared /tmp.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import type { CdpClient } from '../browser/cdp.js';
|
|
15
|
+
import { historyKey, listRuns } from '../report/history.js';
|
|
16
|
+
import { encodePng, pngToDataUrl } from '../report/png.js';
|
|
17
|
+
import type { CaptureData } from '../report/types.js';
|
|
18
|
+
|
|
19
|
+
const collect = vi.hoisted(() => vi.fn());
|
|
20
|
+
vi.mock('../report/collect.js', () => ({ collect }));
|
|
21
|
+
|
|
22
|
+
const { runReport, runReportRepeated, readHistory } = await import('../report/index.js');
|
|
23
|
+
|
|
24
|
+
const URL_UNDER_TEST = 'https://shop.test/checkout';
|
|
25
|
+
const client = {} as CdpClient;
|
|
26
|
+
|
|
27
|
+
function shot(shade: number, width = 8, height = 8) {
|
|
28
|
+
const data = new Uint8Array(width * height * 4);
|
|
29
|
+
for (let i = 0; i < width * height; i++) data.set([shade, shade, shade, 255], i * 4);
|
|
30
|
+
return {
|
|
31
|
+
label: 'page',
|
|
32
|
+
width,
|
|
33
|
+
height,
|
|
34
|
+
dataUrl: pngToDataUrl(encodePng({ width, height, data })),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function capture(partial: Partial<CaptureData> = {}): CaptureData {
|
|
39
|
+
return {
|
|
40
|
+
url: URL_UNDER_TEST,
|
|
41
|
+
finalUrl: URL_UNDER_TEST,
|
|
42
|
+
title: 'Checkout',
|
|
43
|
+
capturedAt: '2026-09-21T10:00:00.000Z',
|
|
44
|
+
durationMs: 1000,
|
|
45
|
+
console: [],
|
|
46
|
+
pageErrors: [],
|
|
47
|
+
requests: [],
|
|
48
|
+
vitals: { lcp: 2000, cls: 0.01, inp: 50 },
|
|
49
|
+
a11y: [],
|
|
50
|
+
structure: [{ path: '/main[1]/button[1]', role: 'button', name: 'Pay now', depth: 1 }],
|
|
51
|
+
screenshots: [shot(200)],
|
|
52
|
+
notes: [],
|
|
53
|
+
...partial,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let root: string;
|
|
58
|
+
let outDir: string;
|
|
59
|
+
|
|
60
|
+
beforeEach(async () => {
|
|
61
|
+
await mkdir(join(homedir(), '.monomind'), { recursive: true });
|
|
62
|
+
root = await mkdtemp(join(homedir(), '.monomind', 'monobrowse-run-test-'));
|
|
63
|
+
outDir = join(root, 'out');
|
|
64
|
+
await mkdir(outDir, { recursive: true });
|
|
65
|
+
collect.mockReset();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(async () => {
|
|
69
|
+
await rm(root, { recursive: true, force: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const run = (data: CaptureData, extra: Record<string, unknown> = {}) => {
|
|
73
|
+
collect.mockResolvedValueOnce(data);
|
|
74
|
+
return runReport(client, 'S1', {
|
|
75
|
+
url: URL_UNDER_TEST,
|
|
76
|
+
out: join(outDir, `${Math.random().toString(36).slice(2)}.html`),
|
|
77
|
+
historyDir: root,
|
|
78
|
+
cwd: outDir,
|
|
79
|
+
...extra,
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
describe('runReport and history', () => {
|
|
84
|
+
it('does not let the first run trend or diff against itself', async () => {
|
|
85
|
+
const result = await run(capture());
|
|
86
|
+
expect(result.report.trend?.runs).toBe(0);
|
|
87
|
+
expect(result.report.diff).toBeUndefined();
|
|
88
|
+
// ...but it is on disk for the next run to compare against.
|
|
89
|
+
expect((await listRuns(join(root, historyKey(URL_UNDER_TEST)))).length).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('trends and diffs the second run against the first', async () => {
|
|
93
|
+
await run(capture());
|
|
94
|
+
const second = await run(
|
|
95
|
+
capture({
|
|
96
|
+
capturedAt: '2026-09-21T11:00:00.000Z',
|
|
97
|
+
vitals: { lcp: 2600, cls: 0.01, inp: 50 },
|
|
98
|
+
structure: [{ path: '/main[1]/button[1]', role: 'button', name: 'Place order', depth: 1 }],
|
|
99
|
+
screenshots: [shot(40)],
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(second.report.trend?.runs).toBe(1);
|
|
104
|
+
const lcp = second.report.trend?.series.find((s) => s.key === 'lcp');
|
|
105
|
+
expect(lcp).toMatchObject({ previous: 2000, current: 2600, direction: 'regressed' });
|
|
106
|
+
|
|
107
|
+
expect(second.report.diff?.previousCapturedAt).toBe('2026-09-21T10:00:00.000Z');
|
|
108
|
+
expect(second.report.diff?.structure?.renamed).toEqual([
|
|
109
|
+
{ path: '/main[1]/button[1]', role: 'button', from: 'Pay now', to: 'Place order' },
|
|
110
|
+
]);
|
|
111
|
+
// Every pixel went from light grey to dark — a whole-screenshot change.
|
|
112
|
+
expect(second.report.diff?.pixels?.changedPercent).toBe(100);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('reports the history directory it wrote to', async () => {
|
|
116
|
+
const result = await run(capture());
|
|
117
|
+
expect(result.historyDir).toBe(join(root, historyKey(URL_UNDER_TEST)));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('prunes to the configured maximum across runs', async () => {
|
|
121
|
+
for (let i = 0; i < 4; i++) {
|
|
122
|
+
await run(capture({ capturedAt: `2026-09-2${i + 1}T10:00:00.000Z` }), { historyMax: 2 });
|
|
123
|
+
}
|
|
124
|
+
const runs = await listRuns(join(root, historyKey(URL_UNDER_TEST)));
|
|
125
|
+
expect(runs.map((r) => r.capturedAt)).toEqual([
|
|
126
|
+
'2026-09-23T10:00:00.000Z',
|
|
127
|
+
'2026-09-24T10:00:00.000Z',
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('writes nothing to the store when history is off, and says nothing about trends', async () => {
|
|
132
|
+
const result = await run(capture(), { history: false });
|
|
133
|
+
expect(result.report.trend).toBeUndefined();
|
|
134
|
+
expect(result.historyDir).toBeUndefined();
|
|
135
|
+
await expect(readdir(join(root, historyKey(URL_UNDER_TEST)))).rejects.toThrow();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('keeps the report when the history store cannot be written', async () => {
|
|
139
|
+
// A regular file where the store's directory needs to be: mkdir fails
|
|
140
|
+
// with ENOTDIR, and the report still has to come out.
|
|
141
|
+
const blocked = join(root, 'blocked-store');
|
|
142
|
+
await writeFile(blocked, 'not a directory', 'utf8');
|
|
143
|
+
const result = await run(capture(), { historyDir: blocked });
|
|
144
|
+
expect(result.report.verdict).toBe('pass');
|
|
145
|
+
expect(result.report.notes.join(' ')).toMatch(/Run history unavailable/);
|
|
146
|
+
expect(result.historyDir).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('exposes the stored runs through readHistory', async () => {
|
|
150
|
+
await run(capture());
|
|
151
|
+
const { runs } = await readHistory(URL_UNDER_TEST, { historyDir: root });
|
|
152
|
+
expect(runs.length).toBe(1);
|
|
153
|
+
expect(runs[0]).toMatchObject({ url: URL_UNDER_TEST, verdict: 'pass' });
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('runReport and evidence', () => {
|
|
158
|
+
const recording = {
|
|
159
|
+
startedAtMs: 1_000_000,
|
|
160
|
+
frames: [
|
|
161
|
+
{ offsetMs: 0, data: 'AAAA', bytes: 4 },
|
|
162
|
+
{ offsetMs: 900, data: 'BBBB', bytes: 4 },
|
|
163
|
+
],
|
|
164
|
+
droppedFrames: 0,
|
|
165
|
+
requested: false,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
it('attaches evidence when the run breaches its budget', async () => {
|
|
169
|
+
const result = await run(
|
|
170
|
+
capture({
|
|
171
|
+
recording,
|
|
172
|
+
console: [{ type: 'error', text: 'boom', timestamp: 1_000_800 }],
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
expect(result.report.verdict).toBe('fail');
|
|
176
|
+
expect(result.report.evidence?.reason).toBe('budget-failure');
|
|
177
|
+
expect(result.report.evidence?.focusOffsetMs).toBe(800);
|
|
178
|
+
expect(result.report.evidence?.frames.length).toBe(2);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('does not attach evidence to a passing run that did not ask for it', async () => {
|
|
182
|
+
const result = await run(capture({ recording }));
|
|
183
|
+
expect(result.report.verdict).toBe('pass');
|
|
184
|
+
expect(result.report.evidence).toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('attaches evidence to a passing run when --record asked for it', async () => {
|
|
188
|
+
const result = await run(capture({ recording: { ...recording, requested: true } }));
|
|
189
|
+
expect(result.report.evidence?.reason).toBe('requested');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('never leaves the raw recording on the report it returns', async () => {
|
|
193
|
+
const result = await run(capture({ recording: { ...recording, requested: true } }));
|
|
194
|
+
expect(result.report.recording).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('keeps the frame images out of the JSON sibling', async () => {
|
|
198
|
+
const result = await run(capture({ recording: { ...recording, requested: true } }));
|
|
199
|
+
const json = JSON.parse(await readFile(result.jsonPath, 'utf8'));
|
|
200
|
+
expect(json.evidence.frames).toEqual([
|
|
201
|
+
{ offsetMs: 0, bytes: 4 },
|
|
202
|
+
{ offsetMs: 900, bytes: 4 },
|
|
203
|
+
]);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('runReportRepeated', () => {
|
|
208
|
+
const failing = capture({ console: [{ type: 'error', text: 'boom', timestamp: 0 }] });
|
|
209
|
+
|
|
210
|
+
it('runs the URL N times and judges the set', async () => {
|
|
211
|
+
for (const data of [capture(), failing, capture()]) collect.mockResolvedValueOnce(data);
|
|
212
|
+
const result = await runReportRepeated(client, 'S1', {
|
|
213
|
+
url: URL_UNDER_TEST,
|
|
214
|
+
out: join(outDir, 'repeat.html'),
|
|
215
|
+
historyDir: root,
|
|
216
|
+
cwd: outDir,
|
|
217
|
+
repeat: 3,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(collect).toHaveBeenCalledTimes(3);
|
|
221
|
+
expect(result.reports.length).toBe(3);
|
|
222
|
+
expect(result.flake.verdict).toBe('flaky');
|
|
223
|
+
expect(result.summary).toMatch(/^FLAKY/);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('refuses to call a sometimes-failing URL a pass, even when the last run is green', async () => {
|
|
227
|
+
for (const data of [failing, capture()]) collect.mockResolvedValueOnce(data);
|
|
228
|
+
const result = await runReportRepeated(client, 'S1', {
|
|
229
|
+
url: URL_UNDER_TEST,
|
|
230
|
+
out: join(outDir, 'repeat2.html'),
|
|
231
|
+
historyDir: root,
|
|
232
|
+
cwd: outDir,
|
|
233
|
+
repeat: 2,
|
|
234
|
+
});
|
|
235
|
+
expect(result.report.verdict).toBe('pass'); // the last run alone
|
|
236
|
+
expect(result.flake.verdict).toBe('flaky'); // what the command reports
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('writes one history entry for the whole command, not one per run', async () => {
|
|
240
|
+
for (const data of [capture(), capture(), capture()]) collect.mockResolvedValueOnce(data);
|
|
241
|
+
await runReportRepeated(client, 'S1', {
|
|
242
|
+
url: URL_UNDER_TEST,
|
|
243
|
+
out: join(outDir, 'repeat3.html'),
|
|
244
|
+
historyDir: root,
|
|
245
|
+
cwd: outDir,
|
|
246
|
+
repeat: 3,
|
|
247
|
+
});
|
|
248
|
+
expect((await listRuns(join(root, historyKey(URL_UNDER_TEST)))).length).toBe(1);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('puts the flake analysis in the JSON sibling', async () => {
|
|
252
|
+
for (const data of [capture(), failing]) collect.mockResolvedValueOnce(data);
|
|
253
|
+
const result = await runReportRepeated(client, 'S1', {
|
|
254
|
+
url: URL_UNDER_TEST,
|
|
255
|
+
out: join(outDir, 'repeat4.html'),
|
|
256
|
+
historyDir: root,
|
|
257
|
+
cwd: outDir,
|
|
258
|
+
repeat: 2,
|
|
259
|
+
});
|
|
260
|
+
const json = JSON.parse(await readFile(result.jsonPath, 'utf8'));
|
|
261
|
+
expect(json.flake.verdict).toBe('flaky');
|
|
262
|
+
expect(json.flake.runs).toBe(2);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -6,23 +6,21 @@
|
|
|
6
6
|
* validation logic: the CdpClient is a stub that answers the handful of CDP
|
|
7
7
|
* commands the round trip issues.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
|
|
10
|
+
import { mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
14
|
import type { CdpClient } from '../browser/cdp.js';
|
|
14
|
-
import {
|
|
15
|
-
saveStateFile,
|
|
16
|
-
loadStateFile,
|
|
17
|
-
saveSession,
|
|
18
|
-
loadSession,
|
|
19
|
-
listSessions,
|
|
20
|
-
} from '../browser/session.js';
|
|
15
|
+
import { loadSession, loadStateFile, saveSession, saveStateFile } from '../browser/session.js';
|
|
21
16
|
|
|
22
17
|
const COOKIES = [{ name: 'sid', value: 'abc', domain: 'x.test', path: '/' }];
|
|
23
18
|
|
|
24
19
|
/** Records every CDP command and answers the ones session.ts depends on. */
|
|
25
|
-
function stubClient(): {
|
|
20
|
+
function stubClient(): {
|
|
21
|
+
client: CdpClient;
|
|
22
|
+
calls: Array<{ method: string; params: unknown; sid?: string }>;
|
|
23
|
+
} {
|
|
26
24
|
const calls: Array<{ method: string; params: unknown; sid?: string }> = [];
|
|
27
25
|
const client = {
|
|
28
26
|
send: vi.fn(async (method: string, params: unknown, sid?: string) => {
|
|
@@ -30,8 +28,10 @@ function stubClient(): { client: CdpClient; calls: Array<{ method: string; param
|
|
|
30
28
|
if (method === 'Network.getCookies') return { cookies: COOKIES };
|
|
31
29
|
if (method === 'Runtime.evaluate') {
|
|
32
30
|
const expr = (params as { expression: string }).expression;
|
|
33
|
-
if (expr.includes('Object.entries(localStorage)'))
|
|
34
|
-
|
|
31
|
+
if (expr.includes('Object.entries(localStorage)'))
|
|
32
|
+
return { result: { value: '{"k":"v"}' } };
|
|
33
|
+
if (expr.includes('Object.entries(sessionStorage)'))
|
|
34
|
+
return { result: { value: '{"s":"1"}' } };
|
|
35
35
|
}
|
|
36
36
|
return {};
|
|
37
37
|
}),
|
|
@@ -69,7 +69,7 @@ describe('validateSessionName (via saveSession/loadSession)', () => {
|
|
|
69
69
|
for (const [label, name] of bad) {
|
|
70
70
|
it(`rejects ${label}`, async () => {
|
|
71
71
|
await expect(saveSession(client, 'S1', 'T1', name, 'https://x.test', 'T')).rejects.toThrow(
|
|
72
|
-
/Invalid session name
|
|
72
|
+
/Invalid session name/,
|
|
73
73
|
);
|
|
74
74
|
await expect(loadSession(client, 'S1', name)).rejects.toThrow(/Invalid session name/);
|
|
75
75
|
});
|
|
@@ -77,7 +77,7 @@ describe('validateSessionName (via saveSession/loadSession)', () => {
|
|
|
77
77
|
|
|
78
78
|
it('accepts an ordinary name (fails later, on the missing file, not on validation)', async () => {
|
|
79
79
|
await expect(loadSession(client, 'S1', 'no-such-session-xyz')).rejects.toThrow(
|
|
80
|
-
'Session not found: no-such-session-xyz'
|
|
80
|
+
'Session not found: no-such-session-xyz',
|
|
81
81
|
);
|
|
82
82
|
});
|
|
83
83
|
|
|
@@ -103,7 +103,7 @@ describe('validateFilePath (via saveStateFile/loadStateFile)', () => {
|
|
|
103
103
|
for (const [label, filePath] of bad) {
|
|
104
104
|
it(`rejects ${label}`, async () => {
|
|
105
105
|
await expect(
|
|
106
|
-
saveStateFile(client, 'S1', 'T1', filePath, 'https://x.test', 'T')
|
|
106
|
+
saveStateFile(client, 'S1', 'T1', filePath, 'https://x.test', 'T'),
|
|
107
107
|
).rejects.toThrow(/Invalid file path/);
|
|
108
108
|
await expect(loadStateFile(client, 'S1', filePath)).rejects.toThrow(/Invalid file path/);
|
|
109
109
|
});
|
|
@@ -111,7 +111,9 @@ describe('validateFilePath (via saveStateFile/loadStateFile)', () => {
|
|
|
111
111
|
|
|
112
112
|
it('allows a plain absolute path', async () => {
|
|
113
113
|
const p = join(dir, 'state.json');
|
|
114
|
-
await expect(
|
|
114
|
+
await expect(
|
|
115
|
+
saveStateFile(client, 'S1', 'T1', p, 'https://x.test', 'T'),
|
|
116
|
+
).resolves.toBeUndefined();
|
|
115
117
|
});
|
|
116
118
|
});
|
|
117
119
|
|
|
@@ -164,38 +166,41 @@ describe('state file round trip', () => {
|
|
|
164
166
|
it('rejects a state file whose cookies field is not an array', async () => {
|
|
165
167
|
const { client } = stubClient();
|
|
166
168
|
const p = join(dir, 'bad.json');
|
|
167
|
-
const { writeFile } = await import('fs/promises');
|
|
169
|
+
const { writeFile } = await import('node:fs/promises');
|
|
168
170
|
await writeFile(p, JSON.stringify({ cookies: { name: 'sid' } }));
|
|
169
171
|
await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
|
|
170
|
-
'Invalid state file: cookies is not an array'
|
|
172
|
+
'Invalid state file: cookies is not an array',
|
|
171
173
|
);
|
|
172
174
|
});
|
|
173
175
|
|
|
174
176
|
it('rejects a state file whose localStorage is an array rather than an object', async () => {
|
|
175
177
|
const { client } = stubClient();
|
|
176
178
|
const p = join(dir, 'bad2.json');
|
|
177
|
-
const { writeFile } = await import('fs/promises');
|
|
179
|
+
const { writeFile } = await import('node:fs/promises');
|
|
178
180
|
await writeFile(p, JSON.stringify({ cookies: [], localStorage: ['nope'] }));
|
|
179
181
|
await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
|
|
180
|
-
'Invalid state file: localStorage is not a plain object'
|
|
182
|
+
'Invalid state file: localStorage is not a plain object',
|
|
181
183
|
);
|
|
182
184
|
});
|
|
183
185
|
|
|
184
186
|
it('rejects a state file whose sessionStorage is an array', async () => {
|
|
185
187
|
const { client } = stubClient();
|
|
186
188
|
const p = join(dir, 'bad3.json');
|
|
187
|
-
const { writeFile } = await import('fs/promises');
|
|
189
|
+
const { writeFile } = await import('node:fs/promises');
|
|
188
190
|
await writeFile(p, JSON.stringify({ cookies: [], sessionStorage: ['nope'] }));
|
|
189
191
|
await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
|
|
190
|
-
'Invalid state file: sessionStorage is not a plain object'
|
|
192
|
+
'Invalid state file: sessionStorage is not a plain object',
|
|
191
193
|
);
|
|
192
194
|
});
|
|
193
195
|
|
|
194
196
|
it('accepts a state file with neither storage map present', async () => {
|
|
195
197
|
const { client, calls } = stubClient();
|
|
196
198
|
const p = join(dir, 'minimal.json');
|
|
197
|
-
const { writeFile } = await import('fs/promises');
|
|
198
|
-
await writeFile(
|
|
199
|
+
const { writeFile } = await import('node:fs/promises');
|
|
200
|
+
await writeFile(
|
|
201
|
+
p,
|
|
202
|
+
JSON.stringify({ targetId: 'T', sessionId: 'S', url: '', title: '', cookies: [] }),
|
|
203
|
+
);
|
|
199
204
|
await expect(loadStateFile(client, 'S1', p)).resolves.toMatchObject({ cookies: [] });
|
|
200
205
|
expect(calls.filter((c) => c.method === 'Runtime.evaluate')).toEqual([]);
|
|
201
206
|
});
|
|
@@ -224,7 +229,7 @@ describe('listSessions', () => {
|
|
|
224
229
|
it('lists only .json files, with the extension stripped', async () => {
|
|
225
230
|
const sessionDir = join(dir, '.monomind', 'browser-sessions');
|
|
226
231
|
await mkdir(sessionDir, { recursive: true });
|
|
227
|
-
const { writeFile } = await import('fs/promises');
|
|
232
|
+
const { writeFile } = await import('node:fs/promises');
|
|
228
233
|
await writeFile(join(sessionDir, 'alpha.json'), '{}');
|
|
229
234
|
await writeFile(join(sessionDir, 'beta.json'), '{}');
|
|
230
235
|
await writeFile(join(sessionDir, 'notes.txt'), 'ignore me');
|
|
@@ -236,7 +241,14 @@ describe('listSessions', () => {
|
|
|
236
241
|
it('saveSession writes into the session dir and loadSession reads it back', async () => {
|
|
237
242
|
const mod = await withHome(dir);
|
|
238
243
|
const saver = stubClient();
|
|
239
|
-
const filePath = await mod.saveSession(
|
|
244
|
+
const filePath = await mod.saveSession(
|
|
245
|
+
saver.client,
|
|
246
|
+
'S1',
|
|
247
|
+
'T1',
|
|
248
|
+
'my-session',
|
|
249
|
+
'https://x.test',
|
|
250
|
+
'T',
|
|
251
|
+
);
|
|
240
252
|
expect(filePath).toBe(join(dir, '.monomind', 'browser-sessions', 'my-session.json'));
|
|
241
253
|
expect((await stat(filePath)).mode & 0o777).toBe(0o600);
|
|
242
254
|
expect((await stat(join(dir, '.monomind', 'browser-sessions'))).mode & 0o777).toBe(0o700);
|