@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
package/dist/src/cli/commands.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Browse Command — Native browser automation via Chrome DevTools Protocol
|
|
3
3
|
* Provides ref-based element model and token-efficient accessibility snapshots
|
|
4
4
|
*/
|
|
5
|
-
import { output } from './output.js';
|
|
6
5
|
import { createActionCommand } from './action.js';
|
|
6
|
+
import { output } from './output.js';
|
|
7
7
|
import { createPlatformCommand } from './platform.js';
|
|
8
8
|
// Runtime state (single session per CLI process)
|
|
9
9
|
const DEFAULT_PORT = 9222;
|
|
@@ -50,7 +50,9 @@ function ensureSignalCleanupHandlers() {
|
|
|
50
50
|
try {
|
|
51
51
|
process.kill(pid, 'SIGKILL');
|
|
52
52
|
}
|
|
53
|
-
catch {
|
|
53
|
+
catch {
|
|
54
|
+
/* already exited */
|
|
55
|
+
}
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
}
|
|
@@ -78,7 +80,9 @@ async function resolveDefaultPort(browser) {
|
|
|
78
80
|
// user's debug port and silently swap which browser commands act on —
|
|
79
81
|
// fail loudly instead.
|
|
80
82
|
try {
|
|
81
|
-
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
83
|
+
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
84
|
+
signal: AbortSignal.timeout(1500),
|
|
85
|
+
});
|
|
82
86
|
}
|
|
83
87
|
catch {
|
|
84
88
|
await browser.clearActivePort();
|
|
@@ -89,7 +93,7 @@ async function resolveDefaultPort(browser) {
|
|
|
89
93
|
}
|
|
90
94
|
async function ensureConnected(port, targetId) {
|
|
91
95
|
const browser = await getBrowser();
|
|
92
|
-
if (!_client
|
|
96
|
+
if (!_client?.isConnected()) {
|
|
93
97
|
if (_client && _sessionId) {
|
|
94
98
|
browser.teardownRouteInterception(_sessionId);
|
|
95
99
|
browser.stopRequestCapture(_sessionId);
|
|
@@ -146,20 +150,28 @@ async function detectAttentionNeeded(client, sessionId, url) {
|
|
|
146
150
|
if (ATTENTION_URL_RE.test(url))
|
|
147
151
|
return 'login';
|
|
148
152
|
try {
|
|
149
|
-
const hasPassword = await client
|
|
153
|
+
const hasPassword = await client
|
|
154
|
+
.send('Runtime.evaluate', {
|
|
150
155
|
expression: '!!document.querySelector("input[type=password]")',
|
|
151
156
|
returnByValue: true,
|
|
152
|
-
}, sessionId)
|
|
157
|
+
}, sessionId)
|
|
158
|
+
.then((r) => r.result?.value === true)
|
|
159
|
+
.catch(() => false);
|
|
153
160
|
if (hasPassword)
|
|
154
161
|
return 'login';
|
|
155
|
-
const hasCaptcha = await client
|
|
162
|
+
const hasCaptcha = await client
|
|
163
|
+
.send('Runtime.evaluate', {
|
|
156
164
|
expression: '!!(document.querySelector("iframe[src*=recaptcha]") || document.querySelector("iframe[src*=hcaptcha]") || document.querySelector(".g-recaptcha") || document.querySelector(".h-captcha") || document.querySelector("[data-sitekey]"))',
|
|
157
165
|
returnByValue: true,
|
|
158
|
-
}, sessionId)
|
|
166
|
+
}, sessionId)
|
|
167
|
+
.then((r) => r.result?.value === true)
|
|
168
|
+
.catch(() => false);
|
|
159
169
|
if (hasCaptcha)
|
|
160
170
|
return 'captcha';
|
|
161
171
|
}
|
|
162
|
-
catch {
|
|
172
|
+
catch {
|
|
173
|
+
/* ignore CDP errors */
|
|
174
|
+
}
|
|
163
175
|
return null;
|
|
164
176
|
}
|
|
165
177
|
function waitForEnter() {
|
|
@@ -167,12 +179,16 @@ function waitForEnter() {
|
|
|
167
179
|
// Non-interactive context (MCP server, piped input, CI) — cannot safely read stdin.
|
|
168
180
|
// Auto-continue after a short grace period so automation isn't blocked.
|
|
169
181
|
output.printWarning('Non-interactive mode: auto-continuing in 30s. Switch to headed manually if needed.');
|
|
170
|
-
return new Promise(resolve => setTimeout(resolve, 30_000));
|
|
182
|
+
return new Promise((resolve) => setTimeout(resolve, 30_000));
|
|
171
183
|
}
|
|
172
|
-
return new Promise(resolve => {
|
|
184
|
+
return new Promise((resolve) => {
|
|
173
185
|
process.stdin.resume();
|
|
174
186
|
process.stdin.setEncoding('utf8');
|
|
175
|
-
const onData = () => {
|
|
187
|
+
const onData = () => {
|
|
188
|
+
process.stdin.pause();
|
|
189
|
+
process.stdin.off('data', onData);
|
|
190
|
+
resolve();
|
|
191
|
+
};
|
|
176
192
|
process.stdin.once('data', onData);
|
|
177
193
|
});
|
|
178
194
|
}
|
|
@@ -184,7 +200,9 @@ async function switchToHeaded(url, port) {
|
|
|
184
200
|
try {
|
|
185
201
|
savedCookies = await browser.getCookies(_client, _sessionId);
|
|
186
202
|
}
|
|
187
|
-
catch {
|
|
203
|
+
catch {
|
|
204
|
+
/* ignore */
|
|
205
|
+
}
|
|
188
206
|
browser.teardownRouteInterception(_sessionId);
|
|
189
207
|
browser.stopRequestCapture(_sessionId);
|
|
190
208
|
browser.teardownDialogHandling(_sessionId);
|
|
@@ -219,14 +237,18 @@ async function switchToHeaded(url, port) {
|
|
|
219
237
|
try {
|
|
220
238
|
await browser.setCookies(_client, _sessionId, savedCookies);
|
|
221
239
|
}
|
|
222
|
-
catch {
|
|
240
|
+
catch {
|
|
241
|
+
/* ignore */
|
|
242
|
+
}
|
|
223
243
|
}
|
|
224
244
|
await browser.openUrl(_client, _sessionId, url);
|
|
225
245
|
output.printInfo('Browser window opened. Complete the required action (login / CAPTCHA), then press Enter to continue in headless mode...');
|
|
226
246
|
await waitForEnter();
|
|
227
247
|
// Capture post-auth cookies
|
|
228
248
|
const authCookies = await browser.getCookies(_client, _sessionId).catch(() => []);
|
|
229
|
-
const authLocalStorage = await browser
|
|
249
|
+
const authLocalStorage = await browser
|
|
250
|
+
.getLocalStorage(_client, _sessionId)
|
|
251
|
+
.catch(() => ({}));
|
|
230
252
|
// Close headed session — actually terminate the underlying Chrome process
|
|
231
253
|
// (Browser.close CDP command, PID-kill fallback), not just our CDP client
|
|
232
254
|
// connection, so the visible authenticated window doesn't linger forever.
|
|
@@ -251,13 +273,17 @@ async function switchToHeaded(url, port) {
|
|
|
251
273
|
try {
|
|
252
274
|
await browser.setCookies(_client, _sessionId, authCookies);
|
|
253
275
|
}
|
|
254
|
-
catch {
|
|
276
|
+
catch {
|
|
277
|
+
/* ignore */
|
|
278
|
+
}
|
|
255
279
|
}
|
|
256
280
|
if (authLocalStorage && Object.keys(authLocalStorage).length) {
|
|
257
281
|
try {
|
|
258
282
|
await browser.setLocalStorage(_client, _sessionId, authLocalStorage);
|
|
259
283
|
}
|
|
260
|
-
catch {
|
|
284
|
+
catch {
|
|
285
|
+
/* ignore */
|
|
286
|
+
}
|
|
261
287
|
}
|
|
262
288
|
}
|
|
263
289
|
/**
|
|
@@ -272,7 +298,7 @@ function imageFormat(value, allowed, fallback) {
|
|
|
272
298
|
return allowed.includes(value) ? value : fallback;
|
|
273
299
|
}
|
|
274
300
|
function print(msg) {
|
|
275
|
-
process.stdout.write(msg
|
|
301
|
+
process.stdout.write(`${msg}\n`);
|
|
276
302
|
}
|
|
277
303
|
// ---------------------------------------------------------------------------
|
|
278
304
|
// Sub-commands
|
|
@@ -282,7 +308,12 @@ const openCommand = {
|
|
|
282
308
|
description: 'Open a URL in the browser. Usage: monomind browse open <url>',
|
|
283
309
|
options: [
|
|
284
310
|
{ name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
|
|
285
|
-
{
|
|
311
|
+
{
|
|
312
|
+
name: 'headed',
|
|
313
|
+
type: 'boolean',
|
|
314
|
+
description: 'Force visible browser window',
|
|
315
|
+
default: false,
|
|
316
|
+
},
|
|
286
317
|
{ name: 'session', short: 's', type: 'string', description: 'Session name to restore' },
|
|
287
318
|
{ name: 'state', type: 'string', description: 'State file to load' },
|
|
288
319
|
],
|
|
@@ -300,19 +331,25 @@ const openCommand = {
|
|
|
300
331
|
try {
|
|
301
332
|
await browser.stopHarRecording(prevClient, prevSid);
|
|
302
333
|
}
|
|
303
|
-
catch {
|
|
334
|
+
catch {
|
|
335
|
+
/* ignore */
|
|
336
|
+
}
|
|
304
337
|
}
|
|
305
338
|
if (browser.getTraceStatus(prevSid)) {
|
|
306
339
|
try {
|
|
307
340
|
await browser.stopTrace(prevClient, prevSid);
|
|
308
341
|
}
|
|
309
|
-
catch {
|
|
342
|
+
catch {
|
|
343
|
+
/* ignore */
|
|
344
|
+
}
|
|
310
345
|
}
|
|
311
346
|
if (browser.isProfilingActive(prevSid)) {
|
|
312
347
|
try {
|
|
313
348
|
await browser.stopCpuProfile(prevClient, prevSid);
|
|
314
349
|
}
|
|
315
|
-
catch {
|
|
350
|
+
catch {
|
|
351
|
+
/* ignore */
|
|
352
|
+
}
|
|
316
353
|
}
|
|
317
354
|
browser.teardownRouteInterception(prevSid);
|
|
318
355
|
browser.stopRequestCapture(prevSid);
|
|
@@ -352,7 +389,10 @@ const openCommand = {
|
|
|
352
389
|
// launched it) instead of overwriting with undefined.
|
|
353
390
|
const existing = await browser.loadActivePortInfo();
|
|
354
391
|
const preserved = existing && existing.port === _port ? existing : undefined;
|
|
355
|
-
await browser.saveActivePort(_port, {
|
|
392
|
+
await browser.saveActivePort(_port, {
|
|
393
|
+
pid: preserved?.pid,
|
|
394
|
+
userDataDir: preserved?.userDataDir,
|
|
395
|
+
});
|
|
356
396
|
}
|
|
357
397
|
ensureSignalCleanupHandlers();
|
|
358
398
|
const conn = await browser.connectToTarget(_port);
|
|
@@ -395,15 +435,49 @@ const snapshotCommand = {
|
|
|
395
435
|
name: 'snapshot',
|
|
396
436
|
description: 'Capture accessibility snapshot with ref-based element handles (@e1, @e2, ...)',
|
|
397
437
|
options: [
|
|
398
|
-
{
|
|
399
|
-
|
|
438
|
+
{
|
|
439
|
+
name: 'interactive',
|
|
440
|
+
short: 'i',
|
|
441
|
+
type: 'boolean',
|
|
442
|
+
description: 'Interactive elements only (93% token reduction)',
|
|
443
|
+
default: false,
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: 'compact',
|
|
447
|
+
short: 'c',
|
|
448
|
+
type: 'boolean',
|
|
449
|
+
description: 'Compact output format',
|
|
450
|
+
default: false,
|
|
451
|
+
},
|
|
400
452
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
401
453
|
{ name: 'depth', short: 'd', type: 'number', description: 'Max depth of AX tree to show' },
|
|
402
|
-
{
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
454
|
+
{
|
|
455
|
+
name: 'selector',
|
|
456
|
+
short: 's',
|
|
457
|
+
type: 'string',
|
|
458
|
+
description: 'Scope snapshot to a CSS selector',
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
name: 'save',
|
|
462
|
+
type: 'string',
|
|
463
|
+
description: 'Save snapshot text to file (baseline for --diff)',
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: 'diff',
|
|
467
|
+
type: 'string',
|
|
468
|
+
description: 'Compare current snapshot against a saved baseline file',
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: 'content-boundaries',
|
|
472
|
+
type: 'boolean',
|
|
473
|
+
description: 'Wrap output in sentinel markers to prevent page-content injection attacks',
|
|
474
|
+
default: false,
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: 'max-output',
|
|
478
|
+
type: 'number',
|
|
479
|
+
description: 'Truncate output to N characters (prevents context window blowout on large pages)',
|
|
480
|
+
},
|
|
407
481
|
],
|
|
408
482
|
action: async (ctx) => {
|
|
409
483
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -427,8 +501,8 @@ const snapshotCommand = {
|
|
|
427
501
|
};
|
|
428
502
|
// --save: write snapshot text to baseline file
|
|
429
503
|
if (ctx.flags.save) {
|
|
430
|
-
const { writeFile, mkdir } = await import('fs/promises');
|
|
431
|
-
const { dirname } = await import('path');
|
|
504
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
505
|
+
const { dirname } = await import('node:path');
|
|
432
506
|
const savePath = ctx.flags.save;
|
|
433
507
|
await mkdir(dirname(savePath), { recursive: true }).catch(() => { });
|
|
434
508
|
await writeFile(savePath, result.text, 'utf8');
|
|
@@ -437,7 +511,7 @@ const snapshotCommand = {
|
|
|
437
511
|
}
|
|
438
512
|
// --diff: compare against baseline file
|
|
439
513
|
if (ctx.flags.diff) {
|
|
440
|
-
const { readFile } = await import('fs/promises');
|
|
514
|
+
const { readFile } = await import('node:fs/promises');
|
|
441
515
|
const baselinePath = ctx.flags.diff;
|
|
442
516
|
let baseline;
|
|
443
517
|
try {
|
|
@@ -459,7 +533,13 @@ const snapshotCommand = {
|
|
|
459
533
|
removed.push(l);
|
|
460
534
|
const changed = added.length > 0 || removed.length > 0;
|
|
461
535
|
if (ctx.flags.json) {
|
|
462
|
-
print(JSON.stringify({
|
|
536
|
+
print(JSON.stringify({
|
|
537
|
+
changed,
|
|
538
|
+
additions: added.length,
|
|
539
|
+
removals: removed.length,
|
|
540
|
+
added,
|
|
541
|
+
removed,
|
|
542
|
+
}));
|
|
463
543
|
}
|
|
464
544
|
else {
|
|
465
545
|
if (!changed) {
|
|
@@ -473,11 +553,19 @@ const snapshotCommand = {
|
|
|
473
553
|
print(`\x1b[31m- ${l}\x1b[0m`);
|
|
474
554
|
}
|
|
475
555
|
}
|
|
476
|
-
return {
|
|
556
|
+
return {
|
|
557
|
+
success: true,
|
|
558
|
+
data: { changed, additions: added.length, removals: removed.length },
|
|
559
|
+
};
|
|
477
560
|
}
|
|
478
561
|
if (ctx.flags.json) {
|
|
479
562
|
const refsObj = Object.fromEntries([...result.refs.entries()].map(([k, v]) => [k, v]));
|
|
480
|
-
print(JSON.stringify({
|
|
563
|
+
print(JSON.stringify({
|
|
564
|
+
url: result.url,
|
|
565
|
+
title: result.title,
|
|
566
|
+
refs: refsObj,
|
|
567
|
+
snapshot: result.text,
|
|
568
|
+
}));
|
|
481
569
|
}
|
|
482
570
|
else {
|
|
483
571
|
print(`[${result.title}] ${result.url}\n`);
|
|
@@ -555,11 +643,19 @@ const waitCommand = {
|
|
|
555
643
|
{ name: 'text', type: 'string', description: 'Wait for text to appear in page' },
|
|
556
644
|
{ name: 'not-text', type: 'string', description: 'Wait for text to disappear from page' },
|
|
557
645
|
{ name: 'selector', type: 'string', description: 'Wait for CSS selector to appear' },
|
|
558
|
-
{
|
|
646
|
+
{
|
|
647
|
+
name: 'load',
|
|
648
|
+
type: 'string',
|
|
649
|
+
description: 'Wait for load event: load|networkidle|domcontentloaded',
|
|
650
|
+
},
|
|
559
651
|
{ name: 'fn', type: 'string', description: 'Wait until JS expression returns truthy' },
|
|
560
652
|
{ name: 'ms', type: 'number', description: 'Wait N milliseconds' },
|
|
561
653
|
{ name: 'timeout', short: 't', type: 'number', description: 'Timeout in ms', default: 30000 },
|
|
562
|
-
{
|
|
654
|
+
{
|
|
655
|
+
name: 'download',
|
|
656
|
+
type: 'string',
|
|
657
|
+
description: 'Wait for a file download to complete and save to path (monitors Browser.downloadProgress events)',
|
|
658
|
+
},
|
|
563
659
|
],
|
|
564
660
|
action: async (ctx) => {
|
|
565
661
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -574,7 +670,9 @@ const waitCommand = {
|
|
|
574
670
|
if (ctx.flags.fn) {
|
|
575
671
|
const expr = ctx.flags.fn;
|
|
576
672
|
const rawTimeout = ctx.flags.timeout ?? 30000;
|
|
577
|
-
const timeout = Number.isFinite(rawTimeout)
|
|
673
|
+
const timeout = Number.isFinite(rawTimeout)
|
|
674
|
+
? Math.max(100, Math.min(rawTimeout, 300_000))
|
|
675
|
+
: 30000; // cap at 5min
|
|
578
676
|
const interval = 200;
|
|
579
677
|
const deadline = Date.now() + timeout;
|
|
580
678
|
while (Date.now() < deadline) {
|
|
@@ -593,7 +691,7 @@ const waitCommand = {
|
|
|
593
691
|
const interval = 200;
|
|
594
692
|
const deadline = Date.now() + timeout;
|
|
595
693
|
while (Date.now() < deadline) {
|
|
596
|
-
const text = await browser.evaluateJs(client, sessionId, 'document.body?.innerText ?? ""');
|
|
694
|
+
const text = (await browser.evaluateJs(client, sessionId, 'document.body?.innerText ?? ""'));
|
|
597
695
|
if (!text.includes(target)) {
|
|
598
696
|
output.printSuccess('Text disappeared');
|
|
599
697
|
return { success: true };
|
|
@@ -604,26 +702,39 @@ const waitCommand = {
|
|
|
604
702
|
}
|
|
605
703
|
if (ctx.flags.download) {
|
|
606
704
|
const savePath = ctx.flags.download;
|
|
607
|
-
const { mkdir } = await import('fs/promises');
|
|
608
|
-
const { dirname, join } = await import('path');
|
|
609
|
-
const { tmpdir } = await import('os');
|
|
705
|
+
const { mkdir } = await import('node:fs/promises');
|
|
706
|
+
const { dirname, join } = await import('node:path');
|
|
707
|
+
const { tmpdir } = await import('node:os');
|
|
610
708
|
const downloadDir = join(tmpdir(), `mm-dl-wait-${Date.now()}`);
|
|
611
709
|
await mkdir(downloadDir, { recursive: true });
|
|
612
|
-
await client
|
|
613
|
-
|
|
614
|
-
|
|
710
|
+
await client
|
|
711
|
+
.send('Browser.setDownloadBehavior', {
|
|
712
|
+
behavior: 'allow',
|
|
713
|
+
downloadPath: downloadDir,
|
|
714
|
+
eventsEnabled: true,
|
|
715
|
+
}, undefined)
|
|
716
|
+
.catch(() => client
|
|
717
|
+
.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath: downloadDir }, sessionId)
|
|
718
|
+
.catch(() => { }));
|
|
615
719
|
const MAX_DOWNLOAD_TIMEOUT = 5 * 60 * 1000; // I6: cap at 5 minutes
|
|
616
720
|
const rawTimeout = Math.min(ctx.flags.timeout ?? 30000, MAX_DOWNLOAD_TIMEOUT);
|
|
617
721
|
const finalPath = await new Promise((resolve, reject) => {
|
|
618
722
|
let guid = '';
|
|
619
723
|
let settled = false;
|
|
620
724
|
// C2: capture off() functions to avoid listener leaks
|
|
621
|
-
const offBegin = client.on('Browser.downloadWillBegin', (params) => {
|
|
725
|
+
const offBegin = client.on('Browser.downloadWillBegin', (params) => {
|
|
726
|
+
guid = params.guid;
|
|
727
|
+
});
|
|
622
728
|
let offProgress;
|
|
623
729
|
// cleanup defined before setTimeout so the timeout callback can call it
|
|
624
730
|
let tid;
|
|
625
731
|
let pollTid;
|
|
626
|
-
const cleanup = () => {
|
|
732
|
+
const cleanup = () => {
|
|
733
|
+
clearTimeout(tid);
|
|
734
|
+
clearInterval(pollTid);
|
|
735
|
+
offBegin?.();
|
|
736
|
+
offProgress?.();
|
|
737
|
+
};
|
|
627
738
|
const finish = (path) => {
|
|
628
739
|
if (settled)
|
|
629
740
|
return;
|
|
@@ -641,7 +752,7 @@ const waitCommand = {
|
|
|
641
752
|
tid = setTimeout(() => fail(new Error('Download timed out')), rawTimeout);
|
|
642
753
|
offProgress = client.on('Browser.downloadProgress', async (params) => {
|
|
643
754
|
if (params.guid === guid && params.state === 'completed') {
|
|
644
|
-
const { readdir, rename, rmdir } = await import('fs/promises');
|
|
755
|
+
const { readdir, rename, rmdir } = await import('node:fs/promises');
|
|
645
756
|
const files = await readdir(downloadDir);
|
|
646
757
|
if (files.length > 0) {
|
|
647
758
|
const src = join(downloadDir, files[0]);
|
|
@@ -671,7 +782,7 @@ const waitCommand = {
|
|
|
671
782
|
if (settled || guid)
|
|
672
783
|
return; // a real CDP event has taken over
|
|
673
784
|
try {
|
|
674
|
-
const { stat } = await import('fs/promises');
|
|
785
|
+
const { stat } = await import('node:fs/promises');
|
|
675
786
|
const st = await stat(savePath);
|
|
676
787
|
if (st.isFile() && st.size > 0 && st.size === lastSize) {
|
|
677
788
|
stableReads++;
|
|
@@ -709,8 +820,18 @@ const screenshotCommand = {
|
|
|
709
820
|
{ name: 'full', type: 'boolean', description: 'Full page screenshot', default: false },
|
|
710
821
|
{ name: 'format', type: 'string', description: 'Format: png|jpeg|webp', default: 'png' },
|
|
711
822
|
{ name: 'quality', type: 'number', description: 'Quality 0-100 for jpeg/webp', default: 80 },
|
|
712
|
-
{
|
|
713
|
-
|
|
823
|
+
{
|
|
824
|
+
name: 'annotate',
|
|
825
|
+
type: 'boolean',
|
|
826
|
+
description: 'Overlay numbered labels keyed to @eN refs from last snapshot (viewport-only; do not combine with --full)',
|
|
827
|
+
default: false,
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
name: 'hide-scrollbars',
|
|
831
|
+
type: 'boolean',
|
|
832
|
+
description: 'Hide native scrollbars via CSS injection before capture',
|
|
833
|
+
default: false,
|
|
834
|
+
},
|
|
714
835
|
{ name: 'json', type: 'boolean', description: 'Output JSON with path', default: false },
|
|
715
836
|
],
|
|
716
837
|
action: async (ctx) => {
|
|
@@ -718,10 +839,12 @@ const screenshotCommand = {
|
|
|
718
839
|
const browser = await getBrowser();
|
|
719
840
|
const hideScrollbars = ctx.flags['hide-scrollbars'];
|
|
720
841
|
if (hideScrollbars) {
|
|
721
|
-
await client
|
|
842
|
+
await client
|
|
843
|
+
.send('Runtime.evaluate', {
|
|
722
844
|
expression: `(function(){var s=document.getElementById('__mm_noscroll__');if(s)return;var el=document.createElement('style');el.id='__mm_noscroll__';el.textContent='*::-webkit-scrollbar{display:none!important}*{scrollbar-width:none!important;-ms-overflow-style:none!important}';document.head.appendChild(el);})()`,
|
|
723
845
|
returnByValue: false,
|
|
724
|
-
}, sessionId)
|
|
846
|
+
}, sessionId)
|
|
847
|
+
.catch(() => { });
|
|
725
848
|
}
|
|
726
849
|
const annotate = ctx.flags.annotate;
|
|
727
850
|
// eslint-disable-next-line prefer-const
|
|
@@ -738,10 +861,12 @@ const screenshotCommand = {
|
|
|
738
861
|
}
|
|
739
862
|
finally {
|
|
740
863
|
if (hideScrollbars) {
|
|
741
|
-
await client
|
|
864
|
+
await client
|
|
865
|
+
.send('Runtime.evaluate', {
|
|
742
866
|
expression: `(function(){var s=document.getElementById('__mm_noscroll__');if(s)s.remove();})()`,
|
|
743
867
|
returnByValue: false,
|
|
744
|
-
}, sessionId)
|
|
868
|
+
}, sessionId)
|
|
869
|
+
.catch(() => { });
|
|
745
870
|
}
|
|
746
871
|
}
|
|
747
872
|
if (ctx.flags.json) {
|
|
@@ -756,9 +881,7 @@ const screenshotCommand = {
|
|
|
756
881
|
const getCommand = {
|
|
757
882
|
name: 'get',
|
|
758
883
|
description: 'Get page info. Usage: monomind browse get url|title|text|html|value|attr|count|box|styles [@ref] [attrName]',
|
|
759
|
-
options: [
|
|
760
|
-
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
761
|
-
],
|
|
884
|
+
options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
|
|
762
885
|
action: async (ctx) => {
|
|
763
886
|
const { client, sessionId } = await ensureConnected(_port);
|
|
764
887
|
const browser = await getBrowser();
|
|
@@ -811,7 +934,8 @@ const getCommand = {
|
|
|
811
934
|
throw new Error('Element not in DOM');
|
|
812
935
|
const r = await client.send('Runtime.callFunctionOn', {
|
|
813
936
|
functionDeclaration: 'function() { return this.value ?? null; }',
|
|
814
|
-
objectId,
|
|
937
|
+
objectId,
|
|
938
|
+
returnByValue: true,
|
|
815
939
|
}, sessionId);
|
|
816
940
|
value = r.result?.value ?? null;
|
|
817
941
|
break;
|
|
@@ -830,7 +954,8 @@ const getCommand = {
|
|
|
830
954
|
throw new Error('Element not in DOM');
|
|
831
955
|
const r = await client.send('Runtime.callFunctionOn', {
|
|
832
956
|
functionDeclaration: `function() { return this.getAttribute(${JSON.stringify(attrName)}); }`,
|
|
833
|
-
objectId,
|
|
957
|
+
objectId,
|
|
958
|
+
returnByValue: true,
|
|
834
959
|
}, sessionId);
|
|
835
960
|
value = r.result?.value ?? null;
|
|
836
961
|
break;
|
|
@@ -867,7 +992,8 @@ const getCommand = {
|
|
|
867
992
|
throw new Error('Element not in DOM');
|
|
868
993
|
const r = await client.send('Runtime.callFunctionOn', {
|
|
869
994
|
functionDeclaration: 'function() { const s = window.getComputedStyle(this); return JSON.stringify(Object.fromEntries([...s].map(k => [k, s.getPropertyValue(k)]))); }',
|
|
870
|
-
objectId,
|
|
995
|
+
objectId,
|
|
996
|
+
returnByValue: true,
|
|
871
997
|
}, sessionId);
|
|
872
998
|
try {
|
|
873
999
|
value = JSON.parse(r.result?.value ?? '{}');
|
|
@@ -895,7 +1021,12 @@ const scrollCommand = {
|
|
|
895
1021
|
options: [
|
|
896
1022
|
{ name: 'amount', short: 'a', type: 'number', description: 'Pixels to scroll', default: 300 },
|
|
897
1023
|
{ name: 'ref', type: 'string', description: 'Element ref to scroll within' },
|
|
898
|
-
{
|
|
1024
|
+
{
|
|
1025
|
+
name: 'selector',
|
|
1026
|
+
short: 's',
|
|
1027
|
+
type: 'string',
|
|
1028
|
+
description: 'CSS selector of element to scroll within',
|
|
1029
|
+
},
|
|
899
1030
|
],
|
|
900
1031
|
action: async (ctx) => {
|
|
901
1032
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -905,12 +1036,14 @@ const scrollCommand = {
|
|
|
905
1036
|
throw new Error('Usage: monomind browse scroll up|down|left|right [amount]');
|
|
906
1037
|
// Support positional amount: scroll down 300
|
|
907
1038
|
const positionalAmount = ctx.args[1] !== undefined ? parseInt(ctx.args[1], 10) : undefined;
|
|
908
|
-
const amount =
|
|
1039
|
+
const amount = positionalAmount && Number.isFinite(positionalAmount)
|
|
1040
|
+
? positionalAmount
|
|
1041
|
+
: (ctx.flags.amount ?? 300);
|
|
909
1042
|
if (ctx.flags.selector) {
|
|
910
1043
|
const sel = ctx.flags.selector;
|
|
911
1044
|
const dx = direction === 'right' ? amount : direction === 'left' ? -amount : 0;
|
|
912
1045
|
const dy = direction === 'down' ? amount : direction === 'up' ? -amount : 0;
|
|
913
|
-
const posJson = await browser.evaluateJs(client, sessionId, `(function(){var el=document.querySelector(${JSON.stringify(sel)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`);
|
|
1046
|
+
const posJson = (await browser.evaluateJs(client, sessionId, `(function(){var el=document.querySelector(${JSON.stringify(sel)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`));
|
|
914
1047
|
if (!posJson)
|
|
915
1048
|
throw new Error(`Selector not found: ${sel}`);
|
|
916
1049
|
const pos = JSON.parse(posJson);
|
|
@@ -958,7 +1091,9 @@ const navigateCommand = {
|
|
|
958
1091
|
expression: direction === 'back' ? 'history.back()' : 'history.forward()',
|
|
959
1092
|
}, sessionId);
|
|
960
1093
|
let fallbackHandle;
|
|
961
|
-
const fallbackPromise = new Promise((r) => {
|
|
1094
|
+
const fallbackPromise = new Promise((r) => {
|
|
1095
|
+
fallbackHandle = setTimeout(r, 2000);
|
|
1096
|
+
});
|
|
962
1097
|
await Promise.race([frameStartedPromise, fallbackPromise]);
|
|
963
1098
|
if (fallbackHandle !== undefined)
|
|
964
1099
|
clearTimeout(fallbackHandle);
|
|
@@ -996,10 +1131,13 @@ const setCommand = {
|
|
|
996
1131
|
const width = parseInt(ctx.args[1], 10);
|
|
997
1132
|
const height = parseInt(ctx.args[2], 10);
|
|
998
1133
|
const dpr = parseFloat(ctx.args[3]) || undefined;
|
|
999
|
-
if (isNaN(width) || isNaN(height))
|
|
1134
|
+
if (Number.isNaN(width) || Number.isNaN(height))
|
|
1000
1135
|
throw new Error('Usage: set viewport <width> <height> [dpr]');
|
|
1001
1136
|
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
1002
|
-
width,
|
|
1137
|
+
width,
|
|
1138
|
+
height,
|
|
1139
|
+
deviceScaleFactor: dpr ?? 1,
|
|
1140
|
+
mobile: false,
|
|
1003
1141
|
}, sessionId);
|
|
1004
1142
|
output.printSuccess(`Viewport set to ${width}x${height}${dpr ? ` @${dpr}x` : ''}`);
|
|
1005
1143
|
break;
|
|
@@ -1016,7 +1154,7 @@ const setCommand = {
|
|
|
1016
1154
|
const lat = parseFloat(ctx.args[1]);
|
|
1017
1155
|
const lon = parseFloat(ctx.args[2]);
|
|
1018
1156
|
const acc = parseFloat(ctx.args[3]) || 100;
|
|
1019
|
-
if (isNaN(lat) || isNaN(lon))
|
|
1157
|
+
if (Number.isNaN(lat) || Number.isNaN(lon))
|
|
1020
1158
|
throw new Error('Usage: set geo <latitude> <longitude> [accuracy]');
|
|
1021
1159
|
await browser.setGeolocation(client, sessionId, lat, lon, acc);
|
|
1022
1160
|
output.printSuccess(`Geolocation set: ${lat}, ${lon}`);
|
|
@@ -1066,7 +1204,11 @@ const stateCommand = {
|
|
|
1066
1204
|
name: 'state',
|
|
1067
1205
|
description: 'Manage browser session state. Usage: monomind browse state save|load|list|rename|clean [name]',
|
|
1068
1206
|
options: [
|
|
1069
|
-
{
|
|
1207
|
+
{
|
|
1208
|
+
name: 'older-than',
|
|
1209
|
+
type: 'number',
|
|
1210
|
+
description: 'For state clean: remove sessions older than N days',
|
|
1211
|
+
},
|
|
1070
1212
|
],
|
|
1071
1213
|
action: async (ctx) => {
|
|
1072
1214
|
const browser = await getBrowser();
|
|
@@ -1123,7 +1265,13 @@ const stateCommand = {
|
|
|
1123
1265
|
const title = await browser.getCurrentTitle(c, sid);
|
|
1124
1266
|
const cookies = await browser.getCookies(c, sid);
|
|
1125
1267
|
const ls = await browser.getAllLocalStorage(c, sid);
|
|
1126
|
-
const info = {
|
|
1268
|
+
const info = {
|
|
1269
|
+
url,
|
|
1270
|
+
title,
|
|
1271
|
+
cookies: cookies.length,
|
|
1272
|
+
localStorage: Object.keys(ls).length,
|
|
1273
|
+
refs: _refs.size,
|
|
1274
|
+
};
|
|
1127
1275
|
print(JSON.stringify(info, null, 2));
|
|
1128
1276
|
return { success: true, data: info };
|
|
1129
1277
|
}
|
|
@@ -1146,14 +1294,14 @@ const stateCommand = {
|
|
|
1146
1294
|
if (!sessions.includes(oldName))
|
|
1147
1295
|
throw new Error(`Session not found: ${oldName}`);
|
|
1148
1296
|
// W1: validate newName to prevent path traversal
|
|
1149
|
-
const { basename: basenameFn } = await import('path');
|
|
1297
|
+
const { basename: basenameFn } = await import('node:path');
|
|
1150
1298
|
const safeName = basenameFn(newName);
|
|
1151
1299
|
if (safeName !== newName || safeName.startsWith('.') || safeName.includes('/')) {
|
|
1152
1300
|
throw new Error('Invalid session name — must not contain path separators or start with "."');
|
|
1153
1301
|
}
|
|
1154
|
-
const { unlink: unlinkRename, readFile, writeFile, mkdir: mkdirRename } = await import('fs/promises');
|
|
1155
|
-
const { join: joinR } = await import('path');
|
|
1156
|
-
const { homedir } = await import('os');
|
|
1302
|
+
const { unlink: unlinkRename, readFile, writeFile, mkdir: mkdirRename, } = await import('node:fs/promises');
|
|
1303
|
+
const { join: joinR } = await import('node:path');
|
|
1304
|
+
const { homedir } = await import('node:os');
|
|
1157
1305
|
const sessionDir = joinR(homedir(), '.monomind', 'browser-sessions');
|
|
1158
1306
|
const oldPath = joinR(sessionDir, `${oldName}.json`);
|
|
1159
1307
|
const newPath = joinR(sessionDir, `${newName}.json`);
|
|
@@ -1167,9 +1315,9 @@ const stateCommand = {
|
|
|
1167
1315
|
}
|
|
1168
1316
|
case 'clean': {
|
|
1169
1317
|
const days = ctx.flags['older-than'] ?? 7;
|
|
1170
|
-
const { unlink, stat } = await import('fs/promises');
|
|
1171
|
-
const { join: joinC } = await import('path');
|
|
1172
|
-
const { homedir: homedirC } = await import('os');
|
|
1318
|
+
const { unlink, stat } = await import('node:fs/promises');
|
|
1319
|
+
const { join: joinC } = await import('node:path');
|
|
1320
|
+
const { homedir: homedirC } = await import('node:os');
|
|
1173
1321
|
const sessionDir = joinC(homedirC(), '.monomind', 'browser-sessions');
|
|
1174
1322
|
const sessions = await browser.listSessions();
|
|
1175
1323
|
const cutoff = Date.now() - days * 86400 * 1000;
|
|
@@ -1200,10 +1348,26 @@ const networkCommand = {
|
|
|
1200
1348
|
{ name: 'status', type: 'number', description: 'HTTP status for fulfill', default: 200 },
|
|
1201
1349
|
{ name: 'headers', type: 'string', description: 'JSON headers object' },
|
|
1202
1350
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1203
|
-
{
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1351
|
+
{
|
|
1352
|
+
name: 'filter',
|
|
1353
|
+
type: 'string',
|
|
1354
|
+
description: 'Filter requests by URL substring (for network requests)',
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
name: 'method',
|
|
1358
|
+
type: 'string',
|
|
1359
|
+
description: 'Filter by HTTP method, e.g. GET, POST (for network requests)',
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
name: 'status-code',
|
|
1363
|
+
type: 'number',
|
|
1364
|
+
description: 'Filter by HTTP status code (for network requests)',
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
name: 'type',
|
|
1368
|
+
type: 'string',
|
|
1369
|
+
description: 'Filter by resource type: xhr|fetch|document|script|stylesheet|image (for network requests)',
|
|
1370
|
+
},
|
|
1207
1371
|
],
|
|
1208
1372
|
action: async (ctx) => {
|
|
1209
1373
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -1216,15 +1380,19 @@ const networkCommand = {
|
|
|
1216
1380
|
const pattern = ctx.flags.pattern;
|
|
1217
1381
|
if (!pattern)
|
|
1218
1382
|
throw new Error('--pattern required for network route');
|
|
1219
|
-
const routes = [
|
|
1383
|
+
const routes = [
|
|
1384
|
+
{
|
|
1220
1385
|
pattern,
|
|
1221
1386
|
action: ctx.flags.abort ? 'abort' : ctx.flags.fulfill ? 'fulfill' : 'continue',
|
|
1222
|
-
response: ctx.flags.fulfill
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1387
|
+
response: ctx.flags.fulfill
|
|
1388
|
+
? {
|
|
1389
|
+
status: ctx.flags.status,
|
|
1390
|
+
body: ctx.flags.fulfill,
|
|
1391
|
+
headers: ctx.flags.headers ? JSON.parse(ctx.flags.headers) : {},
|
|
1392
|
+
}
|
|
1393
|
+
: undefined,
|
|
1394
|
+
},
|
|
1395
|
+
];
|
|
1228
1396
|
await browser.setupRoutes(client, sessionId, routes);
|
|
1229
1397
|
output.printSuccess(`Network route set: ${pattern}`);
|
|
1230
1398
|
break;
|
|
@@ -1275,7 +1443,8 @@ const networkCommand = {
|
|
|
1275
1443
|
if (filterStatus)
|
|
1276
1444
|
reqs = reqs.filter((r) => r.status === filterStatus);
|
|
1277
1445
|
if (filterType)
|
|
1278
|
-
reqs = reqs.filter((r) => r.resourceType === filterType ||
|
|
1446
|
+
reqs = reqs.filter((r) => r.resourceType === filterType ||
|
|
1447
|
+
r.type === filterType);
|
|
1279
1448
|
if (ctx.flags.json)
|
|
1280
1449
|
print(JSON.stringify({ data: reqs }));
|
|
1281
1450
|
else {
|
|
@@ -1293,7 +1462,8 @@ const networkCommand = {
|
|
|
1293
1462
|
if (!reqId)
|
|
1294
1463
|
throw new Error('Usage: monomind browse network request <requestId>');
|
|
1295
1464
|
const reqs = browser.getCapturedRequests(sessionId);
|
|
1296
|
-
const req = reqs.find((r) => r.requestId === reqId ||
|
|
1465
|
+
const req = reqs.find((r) => r.requestId === reqId ||
|
|
1466
|
+
r.id === reqId);
|
|
1297
1467
|
if (!req) {
|
|
1298
1468
|
output.printWarning(`Request not found: ${reqId}`);
|
|
1299
1469
|
return { success: false };
|
|
@@ -1319,16 +1489,29 @@ const DEFAULT_EVAL_MAX_OUTPUT = 50_000;
|
|
|
1319
1489
|
function truncateForOutput(text, maxOutput) {
|
|
1320
1490
|
if (!(maxOutput > 0) || text.length <= maxOutput)
|
|
1321
1491
|
return text;
|
|
1322
|
-
return text.slice(0, maxOutput)
|
|
1492
|
+
return `${text.slice(0, maxOutput)}\n[... truncated at ${maxOutput} chars]`;
|
|
1323
1493
|
}
|
|
1324
1494
|
const evalCommand = {
|
|
1325
1495
|
name: 'eval',
|
|
1326
1496
|
description: 'Evaluate JavaScript in page context. Usage: monomind browse eval "document.title"',
|
|
1327
1497
|
options: [
|
|
1328
1498
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1329
|
-
{
|
|
1330
|
-
|
|
1331
|
-
|
|
1499
|
+
{
|
|
1500
|
+
name: 'stdin',
|
|
1501
|
+
type: 'boolean',
|
|
1502
|
+
description: 'Read JS expression from stdin (heredoc-friendly for multiline scripts)',
|
|
1503
|
+
default: false,
|
|
1504
|
+
},
|
|
1505
|
+
{
|
|
1506
|
+
name: 'max-output',
|
|
1507
|
+
type: 'number',
|
|
1508
|
+
description: `Truncate printed output to N characters (default ${DEFAULT_EVAL_MAX_OUTPUT}; 0 disables truncation)`,
|
|
1509
|
+
},
|
|
1510
|
+
{
|
|
1511
|
+
name: 'timeout',
|
|
1512
|
+
type: 'number',
|
|
1513
|
+
description: 'Max ms to wait for evaluation to settle (default 30000)',
|
|
1514
|
+
},
|
|
1332
1515
|
],
|
|
1333
1516
|
action: async (ctx) => {
|
|
1334
1517
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -1354,6 +1537,254 @@ const evalCommand = {
|
|
|
1354
1537
|
return { success: true, data: { result } };
|
|
1355
1538
|
},
|
|
1356
1539
|
};
|
|
1540
|
+
const reportCommand = {
|
|
1541
|
+
name: 'report',
|
|
1542
|
+
description: 'Test a page and write one self-contained HTML report + sibling JSON. Usage: monomind browse report <url>',
|
|
1543
|
+
options: [
|
|
1544
|
+
{ name: 'out', short: 'o', type: 'string', description: 'Output .html path, or a directory' },
|
|
1545
|
+
{
|
|
1546
|
+
name: 'budget',
|
|
1547
|
+
short: 'b',
|
|
1548
|
+
type: 'string',
|
|
1549
|
+
description: 'Budget JSON file path, or inline JSON',
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
name: 'devices',
|
|
1553
|
+
short: 'd',
|
|
1554
|
+
type: 'string',
|
|
1555
|
+
description: 'Comma-separated device names for the screenshot matrix',
|
|
1556
|
+
},
|
|
1557
|
+
{
|
|
1558
|
+
name: 'wait',
|
|
1559
|
+
short: 'w',
|
|
1560
|
+
type: 'string',
|
|
1561
|
+
description: 'Extra settle step: a CSS selector to wait for, or milliseconds',
|
|
1562
|
+
},
|
|
1563
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
1564
|
+
{
|
|
1565
|
+
name: 'full-page',
|
|
1566
|
+
type: 'boolean',
|
|
1567
|
+
description: 'Full-page screenshot (--no-full-page for viewport only)',
|
|
1568
|
+
default: true,
|
|
1569
|
+
},
|
|
1570
|
+
{
|
|
1571
|
+
name: 'vitals-wait',
|
|
1572
|
+
type: 'number',
|
|
1573
|
+
description: 'Milliseconds to let the web-vitals observers run',
|
|
1574
|
+
default: 2500,
|
|
1575
|
+
},
|
|
1576
|
+
{
|
|
1577
|
+
name: 'timeout',
|
|
1578
|
+
type: 'number',
|
|
1579
|
+
description: 'Milliseconds to wait for the page to go network-idle',
|
|
1580
|
+
default: 20000,
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
name: 'keep-open',
|
|
1584
|
+
type: 'boolean',
|
|
1585
|
+
description: 'Leave the browser running after the report',
|
|
1586
|
+
default: false,
|
|
1587
|
+
},
|
|
1588
|
+
{
|
|
1589
|
+
name: 'repeat',
|
|
1590
|
+
type: 'number',
|
|
1591
|
+
description: 'Run the URL N times and report per-check flake rates (RIG-14)',
|
|
1592
|
+
},
|
|
1593
|
+
{
|
|
1594
|
+
name: 'record',
|
|
1595
|
+
type: 'boolean',
|
|
1596
|
+
description: 'Record frames for the evidence timeline even when the run passes',
|
|
1597
|
+
default: false,
|
|
1598
|
+
},
|
|
1599
|
+
{
|
|
1600
|
+
name: 'history',
|
|
1601
|
+
type: 'boolean',
|
|
1602
|
+
description: 'Print the stored run history for this URL instead of running it',
|
|
1603
|
+
default: false,
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
name: 'save',
|
|
1607
|
+
type: 'boolean',
|
|
1608
|
+
description: 'Save this run to the history store (--no-save to skip)',
|
|
1609
|
+
default: true,
|
|
1610
|
+
},
|
|
1611
|
+
{
|
|
1612
|
+
name: 'history-max',
|
|
1613
|
+
type: 'number',
|
|
1614
|
+
description: 'Runs kept per URL before the oldest are pruned (default 20)',
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
name: 'trend-window',
|
|
1618
|
+
type: 'number',
|
|
1619
|
+
description: 'Prior runs charted in the trend section (default 10)',
|
|
1620
|
+
},
|
|
1621
|
+
],
|
|
1622
|
+
examples: [
|
|
1623
|
+
{ command: 'monomind browse report https://example.com', description: 'Report with defaults' },
|
|
1624
|
+
{
|
|
1625
|
+
command: 'monomind browse report https://example.com --repeat 5',
|
|
1626
|
+
description: 'Run five times and report flake rates',
|
|
1627
|
+
},
|
|
1628
|
+
{
|
|
1629
|
+
command: 'monomind browse report https://example.com --history',
|
|
1630
|
+
description: 'Show the stored run history for a URL',
|
|
1631
|
+
},
|
|
1632
|
+
{
|
|
1633
|
+
command: 'monomind browse report https://example.com --record',
|
|
1634
|
+
description: 'Attach a frame-by-frame evidence timeline',
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
command: 'monomind browse report https://example.com --out ./reports/home.html',
|
|
1638
|
+
description: 'Choose the output path',
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
command: 'monomind browse report https://example.com --budget \'{"lcp":4000}\'',
|
|
1642
|
+
description: 'Relax a budget inline',
|
|
1643
|
+
},
|
|
1644
|
+
{
|
|
1645
|
+
command: 'monomind browse report https://example.com --devices "iPhone 14,iPad"',
|
|
1646
|
+
description: 'Add a device screenshot matrix',
|
|
1647
|
+
},
|
|
1648
|
+
],
|
|
1649
|
+
action: async (ctx) => {
|
|
1650
|
+
const url = ctx.args[0];
|
|
1651
|
+
if (!url)
|
|
1652
|
+
throw new Error('URL required. Usage: monomind browse report <url>');
|
|
1653
|
+
// Imported lazily for the same reason getBrowser() is: this module loads
|
|
1654
|
+
// on every CLI invocation, and nothing but `report` needs the renderer.
|
|
1655
|
+
const { runReport, runReportRepeated, readHistory } = await import('../report/index.js');
|
|
1656
|
+
// --history is a read of the store, so it deliberately never launches a
|
|
1657
|
+
// browser — `report --history` must work offline and on a dead site.
|
|
1658
|
+
if (ctx.flags.history) {
|
|
1659
|
+
const { dir, runs } = await readHistory(url);
|
|
1660
|
+
if (ctx.flags.json) {
|
|
1661
|
+
print(JSON.stringify({ data: { dir, runs } }, null, 2));
|
|
1662
|
+
}
|
|
1663
|
+
else if (!runs.length) {
|
|
1664
|
+
output.printWarning(`No stored runs for ${url}`);
|
|
1665
|
+
print(`Looked in: ${dir}`);
|
|
1666
|
+
}
|
|
1667
|
+
else {
|
|
1668
|
+
print(`${runs.length} run(s) for ${url}`);
|
|
1669
|
+
print(`Stored in: ${dir}`);
|
|
1670
|
+
for (const run of runs) {
|
|
1671
|
+
const lcp = run.vitals.lcp === undefined ? '—' : `${Math.round(run.vitals.lcp)}ms`;
|
|
1672
|
+
print(` ${run.capturedAt} ${run.verdict.toUpperCase().padEnd(4)} ` +
|
|
1673
|
+
`LCP ${lcp.padStart(7)} ${run.counts.consoleErrors} console errors ` +
|
|
1674
|
+
`${run.counts.failedRequests} failed requests ${run.counts.a11yErrors} a11y errors`);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return { success: true, data: { dir, runs } };
|
|
1678
|
+
}
|
|
1679
|
+
const repeatRaw = ctx.flags.repeat;
|
|
1680
|
+
const repeat = typeof repeatRaw === 'number' && Number.isFinite(repeatRaw) && repeatRaw > 1
|
|
1681
|
+
? Math.floor(repeatRaw)
|
|
1682
|
+
: undefined;
|
|
1683
|
+
const { client, sessionId } = await ensureConnected(_port);
|
|
1684
|
+
const browser = await getBrowser();
|
|
1685
|
+
const rawDevices = ctx.flags.devices;
|
|
1686
|
+
const devices = typeof rawDevices === 'string'
|
|
1687
|
+
? rawDevices
|
|
1688
|
+
.split(',')
|
|
1689
|
+
.map((d) => d.trim())
|
|
1690
|
+
.filter(Boolean)
|
|
1691
|
+
: undefined;
|
|
1692
|
+
const runOptions = {
|
|
1693
|
+
url,
|
|
1694
|
+
out: ctx.flags.out,
|
|
1695
|
+
budget: ctx.flags.budget,
|
|
1696
|
+
devices,
|
|
1697
|
+
wait: ctx.flags.wait,
|
|
1698
|
+
vitalsWaitMs: ctx.flags['vitals-wait'],
|
|
1699
|
+
loadTimeoutMs: ctx.flags.timeout,
|
|
1700
|
+
fullPage: ctx.flags['full-page'] !== false,
|
|
1701
|
+
cwd: ctx.cwd,
|
|
1702
|
+
// A failing run records its own evidence without being asked; --record
|
|
1703
|
+
// is for when you want the timeline from a run that passes.
|
|
1704
|
+
record: ctx.flags.record === true,
|
|
1705
|
+
history: ctx.flags.save !== false,
|
|
1706
|
+
historyMax: ctx.flags['history-max'],
|
|
1707
|
+
trendWindow: ctx.flags['trend-window'],
|
|
1708
|
+
};
|
|
1709
|
+
let result;
|
|
1710
|
+
try {
|
|
1711
|
+
result = repeat
|
|
1712
|
+
? await runReportRepeated(client, sessionId, { ...runOptions, repeat })
|
|
1713
|
+
: await runReport(client, sessionId, runOptions);
|
|
1714
|
+
}
|
|
1715
|
+
finally {
|
|
1716
|
+
// A report is a one-shot command — CI should not be left with an
|
|
1717
|
+
// orphan Chrome. Only close a browser THIS process launched: an
|
|
1718
|
+
// attached one belongs to the user's own `open`/`connect` session.
|
|
1719
|
+
if (!ctx.flags['keep-open'] && browser.getLaunchedPid(_port) !== undefined) {
|
|
1720
|
+
browser.stopRequestCapture(sessionId);
|
|
1721
|
+
browser.teardownConsoleCapture(sessionId);
|
|
1722
|
+
try {
|
|
1723
|
+
await browser.closeBrowser(client, _port);
|
|
1724
|
+
}
|
|
1725
|
+
catch {
|
|
1726
|
+
/* best-effort */
|
|
1727
|
+
}
|
|
1728
|
+
_client = null;
|
|
1729
|
+
_sessionId = '';
|
|
1730
|
+
_targetId = '';
|
|
1731
|
+
_refs = new Map();
|
|
1732
|
+
await browser.clearActivePort();
|
|
1733
|
+
await browser.clearRefCache();
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
const { report, htmlPath, jsonPath, summary, historyDir } = result;
|
|
1737
|
+
const { flake } = result;
|
|
1738
|
+
// With --repeat the flake verdict governs: a check that failed 2 of 5
|
|
1739
|
+
// runs must not exit 0 just because the last run happened to be green.
|
|
1740
|
+
const passed = flake ? flake.verdict === 'pass' : report.verdict === 'pass';
|
|
1741
|
+
if (ctx.flags.json) {
|
|
1742
|
+
const { toJsonReport } = await import('../report/index.js');
|
|
1743
|
+
print(JSON.stringify({ data: { ...toJsonReport(report), flake, htmlPath, jsonPath, historyDir } }, null, 2));
|
|
1744
|
+
}
|
|
1745
|
+
else {
|
|
1746
|
+
if (passed)
|
|
1747
|
+
output.printSuccess(summary);
|
|
1748
|
+
else
|
|
1749
|
+
output.printError(summary);
|
|
1750
|
+
if (flake) {
|
|
1751
|
+
print(` ${flake.confidenceNote}`);
|
|
1752
|
+
for (const check of flake.checks.filter((c) => !c.stable)) {
|
|
1753
|
+
print(` ${check.label}: failed ${check.failed} of ${check.runs} runs`);
|
|
1754
|
+
}
|
|
1755
|
+
for (const signal of flake.signals.slice(0, 5)) {
|
|
1756
|
+
print(` ${signal.kind} "${signal.signature}" in ${signal.runs} of ${signal.total} runs`);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
for (const failure of report.failures) {
|
|
1760
|
+
print(` ${failure.budget}: expected ${failure.expected}, got ${failure.actual}`);
|
|
1761
|
+
if (failure.detail)
|
|
1762
|
+
print(` ${failure.detail}`);
|
|
1763
|
+
}
|
|
1764
|
+
for (const headline of report.trend?.headlines ?? [])
|
|
1765
|
+
output.printWarning(headline);
|
|
1766
|
+
if (report.diff?.structure) {
|
|
1767
|
+
const d = report.diff.structure;
|
|
1768
|
+
print(` vs previous run: ${d.gained.length} gained, ${d.lost.length} lost, ${d.renamed.length} renamed`);
|
|
1769
|
+
}
|
|
1770
|
+
if (report.diff?.pixels?.comparable) {
|
|
1771
|
+
print(` vs previous run: ${report.diff.pixels.changedPercent.toFixed(2)}% of pixels changed`);
|
|
1772
|
+
}
|
|
1773
|
+
for (const note of report.notes)
|
|
1774
|
+
output.printWarning(note);
|
|
1775
|
+
print(`Report: ${htmlPath}`);
|
|
1776
|
+
print(`JSON: ${jsonPath}`);
|
|
1777
|
+
if (historyDir)
|
|
1778
|
+
print(`History: ${historyDir}`);
|
|
1779
|
+
}
|
|
1780
|
+
// Non-zero exit is the point of RIG-06 — CI and agents gate on it.
|
|
1781
|
+
return {
|
|
1782
|
+
success: passed,
|
|
1783
|
+
exitCode: passed ? 0 : 1,
|
|
1784
|
+
data: { htmlPath, jsonPath, report, flake },
|
|
1785
|
+
};
|
|
1786
|
+
},
|
|
1787
|
+
};
|
|
1357
1788
|
const closeCommand = {
|
|
1358
1789
|
name: 'close',
|
|
1359
1790
|
description: 'Close the active browser session',
|
|
@@ -1367,19 +1798,25 @@ const closeCommand = {
|
|
|
1367
1798
|
try {
|
|
1368
1799
|
await browser.stopHarRecording(client, sid);
|
|
1369
1800
|
}
|
|
1370
|
-
catch {
|
|
1801
|
+
catch {
|
|
1802
|
+
/* ignore */
|
|
1803
|
+
}
|
|
1371
1804
|
}
|
|
1372
1805
|
if (browser.getTraceStatus(sid)) {
|
|
1373
1806
|
try {
|
|
1374
1807
|
await browser.stopTrace(client, sid);
|
|
1375
1808
|
}
|
|
1376
|
-
catch {
|
|
1809
|
+
catch {
|
|
1810
|
+
/* ignore */
|
|
1811
|
+
}
|
|
1377
1812
|
}
|
|
1378
1813
|
if (browser.isProfilingActive(sid)) {
|
|
1379
1814
|
try {
|
|
1380
1815
|
await browser.stopCpuProfile(client, sid);
|
|
1381
1816
|
}
|
|
1382
|
-
catch {
|
|
1817
|
+
catch {
|
|
1818
|
+
/* ignore */
|
|
1819
|
+
}
|
|
1383
1820
|
}
|
|
1384
1821
|
browser.teardownRouteInterception(sid);
|
|
1385
1822
|
browser.stopRequestCapture(sid);
|
|
@@ -1418,7 +1855,9 @@ const closeCommand = {
|
|
|
1418
1855
|
try {
|
|
1419
1856
|
conn.client.close();
|
|
1420
1857
|
}
|
|
1421
|
-
catch {
|
|
1858
|
+
catch {
|
|
1859
|
+
/* already gone */
|
|
1860
|
+
}
|
|
1422
1861
|
}
|
|
1423
1862
|
// closeBrowser has no PID fallback in a fresh process (launchedPids
|
|
1424
1863
|
// is per-process) — re-probe so we report what actually happened.
|
|
@@ -1428,8 +1867,10 @@ const closeCommand = {
|
|
|
1428
1867
|
const probeDeadline = Date.now() + 3000;
|
|
1429
1868
|
while (stillUp && Date.now() < probeDeadline) {
|
|
1430
1869
|
try {
|
|
1431
|
-
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
1432
|
-
|
|
1870
|
+
await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
|
|
1871
|
+
signal: AbortSignal.timeout(800),
|
|
1872
|
+
});
|
|
1873
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1433
1874
|
}
|
|
1434
1875
|
catch {
|
|
1435
1876
|
stillUp = false;
|
|
@@ -1733,7 +2174,7 @@ const tapCommand = {
|
|
|
1733
2174
|
y = Math.round(box.y);
|
|
1734
2175
|
}
|
|
1735
2176
|
else {
|
|
1736
|
-
const posJson = await browser.evaluateJs(client, sessionId, `(function(){var el=document.querySelector(${JSON.stringify(arg)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`);
|
|
2177
|
+
const posJson = (await browser.evaluateJs(client, sessionId, `(function(){var el=document.querySelector(${JSON.stringify(arg)});if(!el)return null;var r=el.getBoundingClientRect();return JSON.stringify({x:r.left+r.width/2,y:r.top+r.height/2});})()`));
|
|
1737
2178
|
if (!posJson)
|
|
1738
2179
|
throw new Error(`Selector not found: ${arg}`);
|
|
1739
2180
|
const pos = JSON.parse(posJson);
|
|
@@ -1750,9 +2191,25 @@ const swipeCommand = {
|
|
|
1750
2191
|
name: 'swipe',
|
|
1751
2192
|
description: 'Swipe gesture (mobile). Usage: monomind browse swipe up|down|left|right [distance] [--x N] [--y N]',
|
|
1752
2193
|
options: [
|
|
1753
|
-
{
|
|
1754
|
-
|
|
1755
|
-
|
|
2194
|
+
{
|
|
2195
|
+
name: 'x',
|
|
2196
|
+
type: 'number',
|
|
2197
|
+
description: 'Start X coordinate (default: center)',
|
|
2198
|
+
default: 200,
|
|
2199
|
+
},
|
|
2200
|
+
{
|
|
2201
|
+
name: 'y',
|
|
2202
|
+
type: 'number',
|
|
2203
|
+
description: 'Start Y coordinate (default: center)',
|
|
2204
|
+
default: 400,
|
|
2205
|
+
},
|
|
2206
|
+
{
|
|
2207
|
+
name: 'distance',
|
|
2208
|
+
short: 'd',
|
|
2209
|
+
type: 'number',
|
|
2210
|
+
description: 'Swipe distance in pixels',
|
|
2211
|
+
default: 300,
|
|
2212
|
+
},
|
|
1756
2213
|
],
|
|
1757
2214
|
action: async (ctx) => {
|
|
1758
2215
|
const { client, sessionId } = await ensureConnected(_port);
|
|
@@ -1763,14 +2220,16 @@ const swipeCommand = {
|
|
|
1763
2220
|
const startX = ctx.flags.x ?? 200;
|
|
1764
2221
|
const startY = ctx.flags.y ?? 400;
|
|
1765
2222
|
const positionalDistance = ctx.args[1] !== undefined ? parseInt(ctx.args[1], 10) : undefined;
|
|
1766
|
-
const distance =
|
|
2223
|
+
const distance = positionalDistance && Number.isFinite(positionalDistance)
|
|
2224
|
+
? positionalDistance
|
|
2225
|
+
: (ctx.flags.distance ?? 300);
|
|
1767
2226
|
const dx = direction === 'right' ? distance : direction === 'left' ? -distance : 0;
|
|
1768
2227
|
const dy = direction === 'down' ? distance : direction === 'up' ? -distance : 0;
|
|
1769
2228
|
await client.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: startX, y: startY }] }, sessionId);
|
|
1770
2229
|
const steps = 10;
|
|
1771
2230
|
for (let i = 1; i <= steps; i++) {
|
|
1772
|
-
const x = Math.round(startX + dx * i / steps);
|
|
1773
|
-
const y = Math.round(startY + dy * i / steps);
|
|
2231
|
+
const x = Math.round(startX + (dx * i) / steps);
|
|
2232
|
+
const y = Math.round(startY + (dy * i) / steps);
|
|
1774
2233
|
await client.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x, y }] }, sessionId);
|
|
1775
2234
|
await new Promise((r) => setTimeout(r, 16));
|
|
1776
2235
|
}
|
|
@@ -1835,33 +2294,43 @@ const downloadCommand = {
|
|
|
1835
2294
|
name: 'download',
|
|
1836
2295
|
description: 'Click an element and capture the triggered file download. Usage: monomind browse download @e1 ./output.pdf',
|
|
1837
2296
|
options: [
|
|
1838
|
-
{
|
|
2297
|
+
{
|
|
2298
|
+
name: 'timeout',
|
|
2299
|
+
short: 't',
|
|
2300
|
+
type: 'number',
|
|
2301
|
+
description: 'Max wait for download in ms',
|
|
2302
|
+
default: 30000,
|
|
2303
|
+
},
|
|
1839
2304
|
{ name: 'json', type: 'boolean', description: 'Output result as JSON', default: false },
|
|
1840
2305
|
],
|
|
1841
2306
|
action: async (ctx) => {
|
|
1842
2307
|
const { client, sessionId } = await ensureConnected(_port);
|
|
1843
|
-
const
|
|
2308
|
+
const _browser = await getBrowser();
|
|
1844
2309
|
const refOrSel = ctx.args[0];
|
|
1845
2310
|
const savePath = ctx.args[1];
|
|
1846
2311
|
if (!refOrSel || !savePath)
|
|
1847
2312
|
throw new Error('Usage: monomind browse download @e1|selector <save-path>');
|
|
1848
|
-
const { mkdir } = await import('fs/promises');
|
|
1849
|
-
const { dirname } = await import('path');
|
|
1850
|
-
const { tmpdir } = await import('os');
|
|
1851
|
-
const { join } = await import('path');
|
|
2313
|
+
const { mkdir } = await import('node:fs/promises');
|
|
2314
|
+
const { dirname } = await import('node:path');
|
|
2315
|
+
const { tmpdir } = await import('node:os');
|
|
2316
|
+
const { join } = await import('node:path');
|
|
1852
2317
|
const downloadDir = join(tmpdir(), `mm-download-${Date.now()}`);
|
|
1853
2318
|
await mkdir(downloadDir, { recursive: true });
|
|
1854
2319
|
// Enable Page.downloadWillBegin / Page.downloadProgress events
|
|
1855
|
-
await client
|
|
2320
|
+
await client
|
|
2321
|
+
.send('Browser.setDownloadBehavior', {
|
|
1856
2322
|
behavior: 'allow',
|
|
1857
2323
|
downloadPath: downloadDir,
|
|
1858
2324
|
eventsEnabled: true,
|
|
1859
|
-
}, undefined)
|
|
2325
|
+
}, undefined)
|
|
2326
|
+
.catch(() => {
|
|
1860
2327
|
// Fallback: older Chrome API (session-scoped)
|
|
1861
|
-
return client
|
|
2328
|
+
return client
|
|
2329
|
+
.send('Page.setDownloadBehavior', {
|
|
1862
2330
|
behavior: 'allow',
|
|
1863
2331
|
downloadPath: downloadDir,
|
|
1864
|
-
}, sessionId)
|
|
2332
|
+
}, sessionId)
|
|
2333
|
+
.catch(() => { });
|
|
1865
2334
|
});
|
|
1866
2335
|
// Track when download completes
|
|
1867
2336
|
const downloadPromise = new Promise((resolve, reject) => {
|
|
@@ -1873,13 +2342,20 @@ const downloadCommand = {
|
|
|
1873
2342
|
let offProgress;
|
|
1874
2343
|
// cleanup defined before setTimeout so the timeout callback can call it
|
|
1875
2344
|
let timeout;
|
|
1876
|
-
const cleanup = () => {
|
|
1877
|
-
|
|
2345
|
+
const cleanup = () => {
|
|
2346
|
+
clearTimeout(timeout);
|
|
2347
|
+
offBegin?.();
|
|
2348
|
+
offProgress?.();
|
|
2349
|
+
};
|
|
2350
|
+
timeout = setTimeout(() => {
|
|
2351
|
+
cleanup();
|
|
2352
|
+
reject(new Error('Download timed out'));
|
|
2353
|
+
}, ctx.flags.timeout);
|
|
1878
2354
|
offProgress = client.on('Browser.downloadProgress', async (params) => {
|
|
1879
2355
|
if (params.guid === guid && params.state === 'completed') {
|
|
1880
2356
|
cleanup();
|
|
1881
2357
|
// Find the downloaded file in downloadDir
|
|
1882
|
-
const { readdir, rename, rmdir } = await import('fs/promises');
|
|
2358
|
+
const { readdir, rename, rmdir } = await import('node:fs/promises');
|
|
1883
2359
|
const files = await readdir(downloadDir);
|
|
1884
2360
|
if (files.length > 0) {
|
|
1885
2361
|
const src = join(downloadDir, files[0]);
|
|
@@ -2071,9 +2547,7 @@ const frameCommand = {
|
|
|
2071
2547
|
const tabCommand = {
|
|
2072
2548
|
name: 'tab',
|
|
2073
2549
|
description: 'Tab management. Usage: monomind browse tab list|new|close [url]',
|
|
2074
|
-
options: [
|
|
2075
|
-
{ name: 'label', type: 'string', description: 'Label for new tab' },
|
|
2076
|
-
],
|
|
2550
|
+
options: [{ name: 'label', type: 'string', description: 'Label for new tab' }],
|
|
2077
2551
|
action: async (ctx) => {
|
|
2078
2552
|
const { client, sessionId } = await ensureConnected(_port);
|
|
2079
2553
|
const browser = await getBrowser();
|
|
@@ -2102,19 +2576,25 @@ const tabCommand = {
|
|
|
2102
2576
|
try {
|
|
2103
2577
|
await browser.stopHarRecording(client, sid);
|
|
2104
2578
|
}
|
|
2105
|
-
catch {
|
|
2579
|
+
catch {
|
|
2580
|
+
/* ignore */
|
|
2581
|
+
}
|
|
2106
2582
|
}
|
|
2107
2583
|
if (browser.getTraceStatus(sid)) {
|
|
2108
2584
|
try {
|
|
2109
2585
|
await browser.stopTrace(client, sid);
|
|
2110
2586
|
}
|
|
2111
|
-
catch {
|
|
2587
|
+
catch {
|
|
2588
|
+
/* ignore */
|
|
2589
|
+
}
|
|
2112
2590
|
}
|
|
2113
2591
|
if (browser.isProfilingActive(sid)) {
|
|
2114
2592
|
try {
|
|
2115
2593
|
await browser.stopCpuProfile(client, sid);
|
|
2116
2594
|
}
|
|
2117
|
-
catch {
|
|
2595
|
+
catch {
|
|
2596
|
+
/* ignore */
|
|
2597
|
+
}
|
|
2118
2598
|
}
|
|
2119
2599
|
browser.teardownRouteInterception(sid);
|
|
2120
2600
|
browser.stopRequestCapture(sid);
|
|
@@ -2142,19 +2622,25 @@ const tabCommand = {
|
|
|
2142
2622
|
try {
|
|
2143
2623
|
await browser.stopHarRecording(client, oldSid);
|
|
2144
2624
|
}
|
|
2145
|
-
catch {
|
|
2625
|
+
catch {
|
|
2626
|
+
/* ignore */
|
|
2627
|
+
}
|
|
2146
2628
|
}
|
|
2147
2629
|
if (browser.getTraceStatus(oldSid)) {
|
|
2148
2630
|
try {
|
|
2149
2631
|
await browser.stopTrace(client, oldSid);
|
|
2150
2632
|
}
|
|
2151
|
-
catch {
|
|
2633
|
+
catch {
|
|
2634
|
+
/* ignore */
|
|
2635
|
+
}
|
|
2152
2636
|
}
|
|
2153
2637
|
if (browser.isProfilingActive(oldSid)) {
|
|
2154
2638
|
try {
|
|
2155
2639
|
await browser.stopCpuProfile(client, oldSid);
|
|
2156
2640
|
}
|
|
2157
|
-
catch {
|
|
2641
|
+
catch {
|
|
2642
|
+
/* ignore */
|
|
2643
|
+
}
|
|
2158
2644
|
}
|
|
2159
2645
|
await browser.disableInterception(client, oldSid).catch(() => { });
|
|
2160
2646
|
browser.stopRequestCapture(oldSid);
|
|
@@ -2192,19 +2678,25 @@ const windowCommand = {
|
|
|
2192
2678
|
try {
|
|
2193
2679
|
await browser.stopHarRecording(client, oldSid);
|
|
2194
2680
|
}
|
|
2195
|
-
catch {
|
|
2681
|
+
catch {
|
|
2682
|
+
/* ignore */
|
|
2683
|
+
}
|
|
2196
2684
|
}
|
|
2197
2685
|
if (browser.getTraceStatus(oldSid)) {
|
|
2198
2686
|
try {
|
|
2199
2687
|
await browser.stopTrace(client, oldSid);
|
|
2200
2688
|
}
|
|
2201
|
-
catch {
|
|
2689
|
+
catch {
|
|
2690
|
+
/* ignore */
|
|
2691
|
+
}
|
|
2202
2692
|
}
|
|
2203
2693
|
if (browser.isProfilingActive(oldSid)) {
|
|
2204
2694
|
try {
|
|
2205
2695
|
await browser.stopCpuProfile(client, oldSid);
|
|
2206
2696
|
}
|
|
2207
|
-
catch {
|
|
2697
|
+
catch {
|
|
2698
|
+
/* ignore */
|
|
2699
|
+
}
|
|
2208
2700
|
}
|
|
2209
2701
|
browser.teardownRouteInterception(oldSid);
|
|
2210
2702
|
browser.stopRequestCapture(oldSid);
|
|
@@ -2366,11 +2858,13 @@ const cookiesCommand = {
|
|
|
2366
2858
|
if (!name || value === undefined) {
|
|
2367
2859
|
throw new Error('Usage: monomind browse cookies set <name> <value> [--domain <d>]');
|
|
2368
2860
|
}
|
|
2369
|
-
await browser.setCookies(client, sessionId, [
|
|
2861
|
+
await browser.setCookies(client, sessionId, [
|
|
2862
|
+
{
|
|
2370
2863
|
name,
|
|
2371
2864
|
value,
|
|
2372
2865
|
domain: ctx.flags.domain,
|
|
2373
|
-
}
|
|
2866
|
+
},
|
|
2867
|
+
]);
|
|
2374
2868
|
output.printSuccess(`Cookie set: ${name}`);
|
|
2375
2869
|
break;
|
|
2376
2870
|
}
|
|
@@ -2406,9 +2900,7 @@ const pdfCommand = {
|
|
|
2406
2900
|
const isCommand = {
|
|
2407
2901
|
name: 'is',
|
|
2408
2902
|
description: 'Check element state. Usage: monomind browse is visible|enabled|checked @e1',
|
|
2409
|
-
options: [
|
|
2410
|
-
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
2411
|
-
],
|
|
2903
|
+
options: [{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false }],
|
|
2412
2904
|
action: async (ctx) => {
|
|
2413
2905
|
const { client, sessionId } = await ensureConnected(_port);
|
|
2414
2906
|
const browser = await getBrowser();
|
|
@@ -2429,7 +2921,8 @@ const isCommand = {
|
|
|
2429
2921
|
case 'checked':
|
|
2430
2922
|
result = await browser.isChecked(client, sessionId, ref);
|
|
2431
2923
|
break;
|
|
2432
|
-
default:
|
|
2924
|
+
default:
|
|
2925
|
+
throw new Error(`Unknown check: ${check}. Use: visible|enabled|checked`);
|
|
2433
2926
|
}
|
|
2434
2927
|
if (ctx.flags.json) {
|
|
2435
2928
|
print(JSON.stringify({ data: { [check]: result } }));
|
|
@@ -2467,7 +2960,7 @@ const findCommand = {
|
|
|
2467
2960
|
if (locator === 'alttext') {
|
|
2468
2961
|
// I2: use JS attribute comparison to avoid broken CSS selectors for values with spaces/quotes
|
|
2469
2962
|
const valJson = JSON.stringify(value);
|
|
2470
|
-
const found = await browser.evaluateJs(client, sessionId, `(function(v){var el=document.querySelector('img[alt]')||null;var all=document.querySelectorAll('[alt]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('alt')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`);
|
|
2963
|
+
const found = (await browser.evaluateJs(client, sessionId, `(function(v){var el=document.querySelector('img[alt]')||null;var all=document.querySelectorAll('[alt]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('alt')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`));
|
|
2471
2964
|
if (!found) {
|
|
2472
2965
|
output.printWarning(`alttext not found: ${value}`);
|
|
2473
2966
|
return { success: false };
|
|
@@ -2479,7 +2972,7 @@ const findCommand = {
|
|
|
2479
2972
|
if (locator === 'title') {
|
|
2480
2973
|
// I2: use JS attribute comparison to avoid broken CSS selectors for values with spaces/quotes
|
|
2481
2974
|
const valJson = JSON.stringify(value);
|
|
2482
|
-
const found = await browser.evaluateJs(client, sessionId, `(function(v){var all=document.querySelectorAll('[title]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('title')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`);
|
|
2975
|
+
const found = (await browser.evaluateJs(client, sessionId, `(function(v){var all=document.querySelectorAll('[title]');for(var i=0;i<all.length;i++){if(all[i].getAttribute('title')===v){all[i].setAttribute('data-mm-located','true');return true;}}return false;})(${valJson})`));
|
|
2483
2976
|
if (!found) {
|
|
2484
2977
|
output.printWarning(`title not found: ${value}`);
|
|
2485
2978
|
return { success: false };
|
|
@@ -2585,7 +3078,13 @@ const diffCommand = {
|
|
|
2585
3078
|
name: 'diff',
|
|
2586
3079
|
description: 'Compare two URLs or snapshots. Usage: monomind browse diff url <url1> <url2> [--interactive] [--json]',
|
|
2587
3080
|
options: [
|
|
2588
|
-
{
|
|
3081
|
+
{
|
|
3082
|
+
name: 'interactive',
|
|
3083
|
+
short: 'i',
|
|
3084
|
+
type: 'boolean',
|
|
3085
|
+
description: 'Snapshot interactive elements only',
|
|
3086
|
+
default: false,
|
|
3087
|
+
},
|
|
2589
3088
|
{ name: 'json', type: 'boolean', description: 'Output as JSON', default: false },
|
|
2590
3089
|
],
|
|
2591
3090
|
action: async (ctx) => {
|
|
@@ -2600,11 +3099,15 @@ const diffCommand = {
|
|
|
2600
3099
|
// Capture snapshot at url1
|
|
2601
3100
|
await browser.openUrl(client, sessionId, url1);
|
|
2602
3101
|
await browser.waitFor(client, sessionId, { load: 'load', timeout: 15000 });
|
|
2603
|
-
const snap1 = await browser.captureSnapshot(client, sessionId, {
|
|
3102
|
+
const snap1 = await browser.captureSnapshot(client, sessionId, {
|
|
3103
|
+
interactiveOnly: ctx.flags.interactive,
|
|
3104
|
+
});
|
|
2604
3105
|
// Capture snapshot at url2
|
|
2605
3106
|
await browser.openUrl(client, sessionId, url2);
|
|
2606
3107
|
await browser.waitFor(client, sessionId, { load: 'load', timeout: 15000 });
|
|
2607
|
-
const snap2 = await browser.captureSnapshot(client, sessionId, {
|
|
3108
|
+
const snap2 = await browser.captureSnapshot(client, sessionId, {
|
|
3109
|
+
interactiveOnly: ctx.flags.interactive,
|
|
3110
|
+
});
|
|
2608
3111
|
_refs = snap2.refs;
|
|
2609
3112
|
await browser.saveRefCache(_targetId, snap2.url, _refs);
|
|
2610
3113
|
// Text diff
|
|
@@ -2616,7 +3119,15 @@ const diffCommand = {
|
|
|
2616
3119
|
const onlyIn2 = lines2.filter((l) => !set1.has(l));
|
|
2617
3120
|
const changed = onlyIn1.length > 0 || onlyIn2.length > 0;
|
|
2618
3121
|
if (ctx.flags.json) {
|
|
2619
|
-
print(JSON.stringify({
|
|
3122
|
+
print(JSON.stringify({
|
|
3123
|
+
changed,
|
|
3124
|
+
url1,
|
|
3125
|
+
url2,
|
|
3126
|
+
onlyIn1,
|
|
3127
|
+
onlyIn2,
|
|
3128
|
+
additions: onlyIn2.length,
|
|
3129
|
+
removals: onlyIn1.length,
|
|
3130
|
+
}));
|
|
2620
3131
|
}
|
|
2621
3132
|
else {
|
|
2622
3133
|
if (!changed) {
|
|
@@ -2630,7 +3141,10 @@ const diffCommand = {
|
|
|
2630
3141
|
print(`\x1b[32m+ ${l}\x1b[0m`);
|
|
2631
3142
|
}
|
|
2632
3143
|
}
|
|
2633
|
-
return {
|
|
3144
|
+
return {
|
|
3145
|
+
success: true,
|
|
3146
|
+
data: { changed, url1, url2, additions: onlyIn2.length, removals: onlyIn1.length },
|
|
3147
|
+
};
|
|
2634
3148
|
}
|
|
2635
3149
|
throw new Error('Usage: monomind browse diff url <url1> <url2>');
|
|
2636
3150
|
},
|
|
@@ -2744,7 +3258,6 @@ export function parseBatchCommandLine(cmdStr) {
|
|
|
2744
3258
|
flags[boolFlag[1]] = true;
|
|
2745
3259
|
rest = rest.slice(boolFlag[0].length);
|
|
2746
3260
|
consumedAnother = true;
|
|
2747
|
-
continue;
|
|
2748
3261
|
}
|
|
2749
3262
|
}
|
|
2750
3263
|
return { subName: 'eval', subArgs: [rest], flags };
|
|
@@ -2803,7 +3316,9 @@ const batchCommand = {
|
|
|
2803
3316
|
parsedFlags[key] = true;
|
|
2804
3317
|
}
|
|
2805
3318
|
}
|
|
2806
|
-
else if (subArgs[i].startsWith('-') &&
|
|
3319
|
+
else if (subArgs[i].startsWith('-') &&
|
|
3320
|
+
subArgs[i].length === 2 &&
|
|
3321
|
+
/[a-zA-Z]/.test(subArgs[i][1])) {
|
|
2807
3322
|
consumedIndices.add(i);
|
|
2808
3323
|
const shortKey = subArgs[i][1];
|
|
2809
3324
|
const optDef = subCmd.options?.find((o) => o.short === shortKey);
|
|
@@ -2835,7 +3350,11 @@ const batchCommand = {
|
|
|
2835
3350
|
};
|
|
2836
3351
|
const cmdResult = await subCmd.action(fakeCtx);
|
|
2837
3352
|
const succeeded = cmdResult?.success !== false;
|
|
2838
|
-
results.push({
|
|
3353
|
+
results.push({
|
|
3354
|
+
command: cmdStr,
|
|
3355
|
+
success: succeeded,
|
|
3356
|
+
error: succeeded ? undefined : 'Command returned failure',
|
|
3357
|
+
});
|
|
2839
3358
|
if (!succeeded && ctx.flags.bail)
|
|
2840
3359
|
break;
|
|
2841
3360
|
}
|
|
@@ -2886,7 +3405,12 @@ const connectCommand = {
|
|
|
2886
3405
|
options: [
|
|
2887
3406
|
{ name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
|
|
2888
3407
|
{ name: 'target', type: 'string', description: 'Target ID to attach to' },
|
|
2889
|
-
{
|
|
3408
|
+
{
|
|
3409
|
+
name: 'auto-connect',
|
|
3410
|
+
type: 'boolean',
|
|
3411
|
+
description: 'Auto-discover running Chrome on ports 9222 and 9229',
|
|
3412
|
+
default: false,
|
|
3413
|
+
},
|
|
2890
3414
|
],
|
|
2891
3415
|
action: async (ctx) => {
|
|
2892
3416
|
let port = ctx.flags.port ?? 9222;
|
|
@@ -2902,7 +3426,9 @@ const connectCommand = {
|
|
|
2902
3426
|
break;
|
|
2903
3427
|
}
|
|
2904
3428
|
}
|
|
2905
|
-
catch {
|
|
3429
|
+
catch {
|
|
3430
|
+
/* port not open */
|
|
3431
|
+
}
|
|
2906
3432
|
}
|
|
2907
3433
|
if (!found)
|
|
2908
3434
|
throw new Error('No running Chrome instance found. Launch Chrome with --remote-debugging-port or use --port.');
|
|
@@ -2915,19 +3441,25 @@ const connectCommand = {
|
|
|
2915
3441
|
try {
|
|
2916
3442
|
await browser.stopHarRecording(prevClient, prevSid);
|
|
2917
3443
|
}
|
|
2918
|
-
catch {
|
|
3444
|
+
catch {
|
|
3445
|
+
/* ignore */
|
|
3446
|
+
}
|
|
2919
3447
|
}
|
|
2920
3448
|
if (browser.getTraceStatus(prevSid)) {
|
|
2921
3449
|
try {
|
|
2922
3450
|
await browser.stopTrace(prevClient, prevSid);
|
|
2923
3451
|
}
|
|
2924
|
-
catch {
|
|
3452
|
+
catch {
|
|
3453
|
+
/* ignore */
|
|
3454
|
+
}
|
|
2925
3455
|
}
|
|
2926
3456
|
if (browser.isProfilingActive(prevSid)) {
|
|
2927
3457
|
try {
|
|
2928
3458
|
await browser.stopCpuProfile(prevClient, prevSid);
|
|
2929
3459
|
}
|
|
2930
|
-
catch {
|
|
3460
|
+
catch {
|
|
3461
|
+
/* ignore */
|
|
3462
|
+
}
|
|
2931
3463
|
}
|
|
2932
3464
|
browser.teardownRouteInterception(prevSid);
|
|
2933
3465
|
browser.stopRequestCapture(prevSid);
|
|
@@ -3030,7 +3562,9 @@ const traceCommand = {
|
|
|
3030
3562
|
throw new Error('Usage: monomind browse trace start|stop [path]');
|
|
3031
3563
|
switch (action) {
|
|
3032
3564
|
case 'start':
|
|
3033
|
-
await browser.startTrace(client, sessionId, {
|
|
3565
|
+
await browser.startTrace(client, sessionId, {
|
|
3566
|
+
screenshots: ctx.flags.screenshots,
|
|
3567
|
+
});
|
|
3034
3568
|
output.printSuccess('Trace started');
|
|
3035
3569
|
return { success: true };
|
|
3036
3570
|
case 'stop': {
|
|
@@ -3064,7 +3598,9 @@ const profilerCommand = {
|
|
|
3064
3598
|
throw new Error('Usage: monomind browse profiler start|stop|heap [path]');
|
|
3065
3599
|
switch (action) {
|
|
3066
3600
|
case 'start':
|
|
3067
|
-
await browser.startCpuProfile(client, sessionId, {
|
|
3601
|
+
await browser.startCpuProfile(client, sessionId, {
|
|
3602
|
+
samplingInterval: ctx.flags.interval,
|
|
3603
|
+
});
|
|
3068
3604
|
output.printSuccess('CPU profiler started');
|
|
3069
3605
|
return { success: true };
|
|
3070
3606
|
case 'stop': {
|
|
@@ -3155,7 +3691,7 @@ const resizeCommand = {
|
|
|
3155
3691
|
const browser = await getBrowser();
|
|
3156
3692
|
const width = parseInt(ctx.args[0], 10);
|
|
3157
3693
|
const height = parseInt(ctx.args[1], 10);
|
|
3158
|
-
if (isNaN(width) || isNaN(height))
|
|
3694
|
+
if (Number.isNaN(width) || Number.isNaN(height))
|
|
3159
3695
|
throw new Error('Usage: monomind browse resize <width> <height>');
|
|
3160
3696
|
await browser.setViewport(client, sessionId, width, height);
|
|
3161
3697
|
output.printSuccess(`Resized to ${width}x${height}`);
|
|
@@ -3291,6 +3827,7 @@ const browseCommand = {
|
|
|
3291
3827
|
traceCommand,
|
|
3292
3828
|
profilerCommand,
|
|
3293
3829
|
vitalsCommand,
|
|
3830
|
+
reportCommand,
|
|
3294
3831
|
harCommand,
|
|
3295
3832
|
resizeCommand,
|
|
3296
3833
|
closeCommand,
|
|
@@ -3303,7 +3840,10 @@ const browseCommand = {
|
|
|
3303
3840
|
],
|
|
3304
3841
|
examples: [
|
|
3305
3842
|
{ command: 'monomind browse open https://example.com', description: 'Open a URL' },
|
|
3306
|
-
{
|
|
3843
|
+
{
|
|
3844
|
+
command: 'monomind browse snapshot -i',
|
|
3845
|
+
description: 'Interactive-only snapshot (93% token reduction)',
|
|
3846
|
+
},
|
|
3307
3847
|
{ command: 'monomind browse click @e3', description: 'Click element by ref' },
|
|
3308
3848
|
{ command: 'monomind browse fill @e1 "user@example.com"', description: 'Fill an input' },
|
|
3309
3849
|
{ command: 'monomind browse press Enter', description: 'Press Enter key' },
|
|
@@ -3317,7 +3857,14 @@ const browseCommand = {
|
|
|
3317
3857
|
{ command: 'monomind browse state save my-session', description: 'Save session state' },
|
|
3318
3858
|
{ command: 'monomind browse navigate back', description: 'Navigate back' },
|
|
3319
3859
|
{ command: 'monomind browse eval "document.title"', description: 'Evaluate JavaScript' },
|
|
3320
|
-
{
|
|
3860
|
+
{
|
|
3861
|
+
command: 'monomind browse network route --pattern "https://api.*" --abort',
|
|
3862
|
+
description: 'Abort API calls',
|
|
3863
|
+
},
|
|
3864
|
+
{
|
|
3865
|
+
command: 'monomind browse report https://example.com',
|
|
3866
|
+
description: 'One-command page report with a pass/fail verdict',
|
|
3867
|
+
},
|
|
3321
3868
|
{ command: 'monomind browse close', description: 'Close browser session' },
|
|
3322
3869
|
],
|
|
3323
3870
|
action: async (_ctx) => {
|
|
@@ -3374,6 +3921,7 @@ const browseCommand = {
|
|
|
3374
3921
|
output.printInfo(' batch Execute multiple commands');
|
|
3375
3922
|
output.printInfo(' addinitscript Add script to run before page navigation');
|
|
3376
3923
|
output.printInfo(' removeinitscript Remove a previously added init script');
|
|
3924
|
+
output.printInfo(' report Test a page and write an HTML report');
|
|
3377
3925
|
output.printInfo(' close Close the browser session');
|
|
3378
3926
|
return { success: true };
|
|
3379
3927
|
},
|