devharness 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/build/atomic-write.d.ts +28 -0
- package/build/atomic-write.d.ts.map +1 -0
- package/build/atomic-write.js +126 -0
- package/build/atomic-write.js.map +1 -0
- package/build/cdp-manager.d.ts +524 -0
- package/build/cdp-manager.d.ts.map +1 -0
- package/build/cdp-manager.js +1828 -0
- package/build/cdp-manager.js.map +1 -0
- package/build/cdp-manager.test.d.ts +6 -0
- package/build/cdp-manager.test.d.ts.map +1 -0
- package/build/cdp-manager.test.js +546 -0
- package/build/cdp-manager.test.js.map +1 -0
- package/build/chrome-launcher.d.ts +400 -0
- package/build/chrome-launcher.d.ts.map +1 -0
- package/build/chrome-launcher.js +1071 -0
- package/build/chrome-launcher.js.map +1 -0
- package/build/chrome-launcher.test.d.ts +11 -0
- package/build/chrome-launcher.test.d.ts.map +1 -0
- package/build/chrome-launcher.test.js +204 -0
- package/build/chrome-launcher.test.js.map +1 -0
- package/build/clickable-cache.d.ts +73 -0
- package/build/clickable-cache.d.ts.map +1 -0
- package/build/clickable-cache.js +90 -0
- package/build/clickable-cache.js.map +1 -0
- package/build/command-recorder.d.ts +401 -0
- package/build/command-recorder.d.ts.map +1 -0
- package/build/command-recorder.js +701 -0
- package/build/command-recorder.js.map +1 -0
- package/build/config.d.ts +390 -0
- package/build/config.d.ts.map +1 -0
- package/build/config.js +826 -0
- package/build/config.js.map +1 -0
- package/build/config.test.d.ts +2 -0
- package/build/config.test.d.ts.map +1 -0
- package/build/config.test.js +112 -0
- package/build/config.test.js.map +1 -0
- package/build/connection-manager.d.ts +159 -0
- package/build/connection-manager.d.ts.map +1 -0
- package/build/connection-manager.js +432 -0
- package/build/connection-manager.js.map +1 -0
- package/build/connection-manager.test.d.ts +11 -0
- package/build/connection-manager.test.d.ts.map +1 -0
- package/build/connection-manager.test.js +114 -0
- package/build/connection-manager.test.js.map +1 -0
- package/build/console-monitor.d.ts +150 -0
- package/build/console-monitor.d.ts.map +1 -0
- package/build/console-monitor.js +454 -0
- package/build/console-monitor.js.map +1 -0
- package/build/dashboard/bundle.js +1 -0
- package/build/dashboard/dashboard-client.d.ts +42 -0
- package/build/dashboard/dashboard-client.d.ts.map +1 -0
- package/build/dashboard/dashboard-client.js +184 -0
- package/build/dashboard/dashboard-client.js.map +1 -0
- package/build/dashboard/dashboard-hub.d.ts +50 -0
- package/build/dashboard/dashboard-hub.d.ts.map +1 -0
- package/build/dashboard/dashboard-hub.js +654 -0
- package/build/dashboard/dashboard-hub.js.map +1 -0
- package/build/dashboard/hub-lock.d.ts +16 -0
- package/build/dashboard/hub-lock.d.ts.map +1 -0
- package/build/dashboard/hub-lock.js +77 -0
- package/build/dashboard/hub-lock.js.map +1 -0
- package/build/dashboard/index.d.ts +25 -0
- package/build/dashboard/index.d.ts.map +1 -0
- package/build/dashboard/index.js +77 -0
- package/build/dashboard/index.js.map +1 -0
- package/build/dashboard/types.d.ts +83 -0
- package/build/dashboard/types.d.ts.map +1 -0
- package/build/dashboard/types.js +6 -0
- package/build/dashboard/types.js.map +1 -0
- package/build/debug-logger.d.ts +85 -0
- package/build/debug-logger.d.ts.map +1 -0
- package/build/debug-logger.js +195 -0
- package/build/debug-logger.js.map +1 -0
- package/build/debugger-aware-wrapper.d.ts +37 -0
- package/build/debugger-aware-wrapper.d.ts.map +1 -0
- package/build/debugger-aware-wrapper.js +128 -0
- package/build/debugger-aware-wrapper.js.map +1 -0
- package/build/dom-change-monitor.d.ts +92 -0
- package/build/dom-change-monitor.d.ts.map +1 -0
- package/build/dom-change-monitor.js +437 -0
- package/build/dom-change-monitor.js.map +1 -0
- package/build/element-collector.d.ts +16 -0
- package/build/element-collector.d.ts.map +1 -0
- package/build/element-collector.js +207 -0
- package/build/element-collector.js.map +1 -0
- package/build/error-helpers.d.ts +43 -0
- package/build/error-helpers.d.ts.map +1 -0
- package/build/error-helpers.js +143 -0
- package/build/error-helpers.js.map +1 -0
- package/build/formatters/console-formatter.d.ts +160 -0
- package/build/formatters/console-formatter.d.ts.map +1 -0
- package/build/formatters/console-formatter.js +338 -0
- package/build/formatters/console-formatter.js.map +1 -0
- package/build/helpers/parser-plugins.d.ts +44 -0
- package/build/helpers/parser-plugins.d.ts.map +1 -0
- package/build/helpers/parser-plugins.js +102 -0
- package/build/helpers/parser-plugins.js.map +1 -0
- package/build/helpers/paths.d.ts +79 -0
- package/build/helpers/paths.d.ts.map +1 -0
- package/build/helpers/paths.js +168 -0
- package/build/helpers/paths.js.map +1 -0
- package/build/helpers/process-liveness.d.ts +24 -0
- package/build/helpers/process-liveness.d.ts.map +1 -0
- package/build/helpers/process-liveness.js +36 -0
- package/build/helpers/process-liveness.js.map +1 -0
- package/build/helpers/process-liveness.test.d.ts +2 -0
- package/build/helpers/process-liveness.test.d.ts.map +1 -0
- package/build/helpers/process-liveness.test.js +40 -0
- package/build/helpers/process-liveness.test.js.map +1 -0
- package/build/helpers/sequence-tree.d.ts +27 -0
- package/build/helpers/sequence-tree.d.ts.map +1 -0
- package/build/helpers/sequence-tree.js +80 -0
- package/build/helpers/sequence-tree.js.map +1 -0
- package/build/helpers/sequence-tree.test.d.ts +2 -0
- package/build/helpers/sequence-tree.test.d.ts.map +1 -0
- package/build/helpers/sequence-tree.test.js +122 -0
- package/build/helpers/sequence-tree.test.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +2007 -0
- package/build/index.js.map +1 -0
- package/build/interaction-recorder.d.ts +214 -0
- package/build/interaction-recorder.d.ts.map +1 -0
- package/build/interaction-recorder.js +2007 -0
- package/build/interaction-recorder.js.map +1 -0
- package/build/issue-tracker.d.ts +107 -0
- package/build/issue-tracker.d.ts.map +1 -0
- package/build/issue-tracker.js +582 -0
- package/build/issue-tracker.js.map +1 -0
- package/build/issue-tracker.test.d.ts +2 -0
- package/build/issue-tracker.test.d.ts.map +1 -0
- package/build/issue-tracker.test.js +88 -0
- package/build/issue-tracker.test.js.map +1 -0
- package/build/launch-chrome-port.test.d.ts +14 -0
- package/build/launch-chrome-port.test.d.ts.map +1 -0
- package/build/launch-chrome-port.test.js +97 -0
- package/build/launch-chrome-port.test.js.map +1 -0
- package/build/log-processor/orchestrator.d.ts +32 -0
- package/build/log-processor/orchestrator.d.ts.map +1 -0
- package/build/log-processor/orchestrator.js +27 -0
- package/build/log-processor/orchestrator.js.map +1 -0
- package/build/logpoint-execution-tracker.d.ts +57 -0
- package/build/logpoint-execution-tracker.d.ts.map +1 -0
- package/build/logpoint-execution-tracker.js +112 -0
- package/build/logpoint-execution-tracker.js.map +1 -0
- package/build/mcp-supervisor.d.ts +3 -0
- package/build/mcp-supervisor.d.ts.map +1 -0
- package/build/mcp-supervisor.js +211 -0
- package/build/mcp-supervisor.js.map +1 -0
- package/build/messages.d.ts +165 -0
- package/build/messages.d.ts.map +1 -0
- package/build/messages.js +574 -0
- package/build/messages.js.map +1 -0
- package/build/network-monitor.d.ts +172 -0
- package/build/network-monitor.d.ts.map +1 -0
- package/build/network-monitor.js +350 -0
- package/build/network-monitor.js.map +1 -0
- package/build/overlays.d.ts +49 -0
- package/build/overlays.d.ts.map +1 -0
- package/build/overlays.js +409 -0
- package/build/overlays.js.map +1 -0
- package/build/persistent-profiles.test.d.ts +15 -0
- package/build/persistent-profiles.test.d.ts.map +1 -0
- package/build/persistent-profiles.test.js +470 -0
- package/build/persistent-profiles.test.js.map +1 -0
- package/build/port-reserver.d.ts +32 -0
- package/build/port-reserver.d.ts.map +1 -0
- package/build/port-reserver.js +118 -0
- package/build/port-reserver.js.map +1 -0
- package/build/puppeteer-manager.d.ts +47 -0
- package/build/puppeteer-manager.d.ts.map +1 -0
- package/build/puppeteer-manager.js +116 -0
- package/build/puppeteer-manager.js.map +1 -0
- package/build/recording-coverage.test.d.ts +12 -0
- package/build/recording-coverage.test.d.ts.map +1 -0
- package/build/recording-coverage.test.js +72 -0
- package/build/recording-coverage.test.js.map +1 -0
- package/build/reference-validator.d.ts +47 -0
- package/build/reference-validator.d.ts.map +1 -0
- package/build/reference-validator.js +86 -0
- package/build/reference-validator.js.map +1 -0
- package/build/reference-validator.test.d.ts +5 -0
- package/build/reference-validator.test.d.ts.map +1 -0
- package/build/reference-validator.test.js +243 -0
- package/build/reference-validator.test.js.map +1 -0
- package/build/replay-cursor.d.ts +27 -0
- package/build/replay-cursor.d.ts.map +1 -0
- package/build/replay-cursor.js +240 -0
- package/build/replay-cursor.js.map +1 -0
- package/build/runners/docker-compose-runner.d.ts +69 -0
- package/build/runners/docker-compose-runner.d.ts.map +1 -0
- package/build/runners/docker-compose-runner.js +438 -0
- package/build/runners/docker-compose-runner.js.map +1 -0
- package/build/runners/docker-runner.d.ts +65 -0
- package/build/runners/docker-runner.d.ts.map +1 -0
- package/build/runners/docker-runner.js +414 -0
- package/build/runners/docker-runner.js.map +1 -0
- package/build/runners/index.d.ts +19 -0
- package/build/runners/index.d.ts.map +1 -0
- package/build/runners/index.js +35 -0
- package/build/runners/index.js.map +1 -0
- package/build/runners/native-runner.d.ts +95 -0
- package/build/runners/native-runner.d.ts.map +1 -0
- package/build/runners/native-runner.js +370 -0
- package/build/runners/native-runner.js.map +1 -0
- package/build/runners/types.d.ts +147 -0
- package/build/runners/types.d.ts.map +1 -0
- package/build/runners/types.js +25 -0
- package/build/runners/types.js.map +1 -0
- package/build/self-restart.d.ts +12 -0
- package/build/self-restart.d.ts.map +1 -0
- package/build/self-restart.js +41 -0
- package/build/self-restart.js.map +1 -0
- package/build/self-restart.test.d.ts +2 -0
- package/build/self-restart.test.d.ts.map +1 -0
- package/build/self-restart.test.js +34 -0
- package/build/self-restart.test.js.map +1 -0
- package/build/server-claims.d.ts +161 -0
- package/build/server-claims.d.ts.map +1 -0
- package/build/server-claims.js +382 -0
- package/build/server-claims.js.map +1 -0
- package/build/server-claims.test.d.ts +2 -0
- package/build/server-claims.test.d.ts.map +1 -0
- package/build/server-claims.test.js +197 -0
- package/build/server-claims.test.js.map +1 -0
- package/build/server-manager.d.ts +438 -0
- package/build/server-manager.d.ts.map +1 -0
- package/build/server-manager.js +1674 -0
- package/build/server-manager.js.map +1 -0
- package/build/server-manager.test.d.ts +17 -0
- package/build/server-manager.test.d.ts.map +1 -0
- package/build/server-manager.test.js +122 -0
- package/build/server-manager.test.js.map +1 -0
- package/build/server-ownership.test.d.ts +2 -0
- package/build/server-ownership.test.d.ts.map +1 -0
- package/build/server-ownership.test.js +238 -0
- package/build/server-ownership.test.js.map +1 -0
- package/build/server-watcher.d.ts +21 -0
- package/build/server-watcher.d.ts.map +1 -0
- package/build/server-watcher.js +105 -0
- package/build/server-watcher.js.map +1 -0
- package/build/session-detector.d.ts +47 -0
- package/build/session-detector.d.ts.map +1 -0
- package/build/session-detector.js +229 -0
- package/build/session-detector.js.map +1 -0
- package/build/session-detector.test.d.ts +9 -0
- package/build/session-detector.test.d.ts.map +1 -0
- package/build/session-detector.test.js +45 -0
- package/build/session-detector.test.js.map +1 -0
- package/build/sourcemap-handler.d.ts +114 -0
- package/build/sourcemap-handler.d.ts.map +1 -0
- package/build/sourcemap-handler.js +479 -0
- package/build/sourcemap-handler.js.map +1 -0
- package/build/supervisor/child-manager.d.ts +43 -0
- package/build/supervisor/child-manager.d.ts.map +1 -0
- package/build/supervisor/child-manager.js +148 -0
- package/build/supervisor/child-manager.js.map +1 -0
- package/build/supervisor/child-manager.test.d.ts +2 -0
- package/build/supervisor/child-manager.test.d.ts.map +1 -0
- package/build/supervisor/child-manager.test.js +107 -0
- package/build/supervisor/child-manager.test.js.map +1 -0
- package/build/supervisor/client-watcher.d.ts +48 -0
- package/build/supervisor/client-watcher.d.ts.map +1 -0
- package/build/supervisor/client-watcher.js +186 -0
- package/build/supervisor/client-watcher.js.map +1 -0
- package/build/supervisor/client-watcher.test.d.ts +2 -0
- package/build/supervisor/client-watcher.test.d.ts.map +1 -0
- package/build/supervisor/client-watcher.test.js +121 -0
- package/build/supervisor/client-watcher.test.js.map +1 -0
- package/build/supervisor/idle-config.d.ts +30 -0
- package/build/supervisor/idle-config.d.ts.map +1 -0
- package/build/supervisor/idle-config.js +76 -0
- package/build/supervisor/idle-config.js.map +1 -0
- package/build/supervisor/idle-config.test.d.ts +2 -0
- package/build/supervisor/idle-config.test.d.ts.map +1 -0
- package/build/supervisor/idle-config.test.js +119 -0
- package/build/supervisor/idle-config.test.js.map +1 -0
- package/build/supervisor/ndjson-reader.d.ts +47 -0
- package/build/supervisor/ndjson-reader.d.ts.map +1 -0
- package/build/supervisor/ndjson-reader.js +75 -0
- package/build/supervisor/ndjson-reader.js.map +1 -0
- package/build/supervisor/ndjson-reader.test.d.ts +2 -0
- package/build/supervisor/ndjson-reader.test.d.ts.map +1 -0
- package/build/supervisor/ndjson-reader.test.js +92 -0
- package/build/supervisor/ndjson-reader.test.js.map +1 -0
- package/build/supervisor/pidfile.d.ts +11 -0
- package/build/supervisor/pidfile.d.ts.map +1 -0
- package/build/supervisor/pidfile.js +29 -0
- package/build/supervisor/pidfile.js.map +1 -0
- package/build/supervisor/pidfile.test.d.ts +8 -0
- package/build/supervisor/pidfile.test.d.ts.map +1 -0
- package/build/supervisor/pidfile.test.js +46 -0
- package/build/supervisor/pidfile.test.js.map +1 -0
- package/build/supervisor/restart-coordinator.d.ts +73 -0
- package/build/supervisor/restart-coordinator.d.ts.map +1 -0
- package/build/supervisor/restart-coordinator.js +300 -0
- package/build/supervisor/restart-coordinator.js.map +1 -0
- package/build/supervisor/restart-coordinator.test.d.ts +2 -0
- package/build/supervisor/restart-coordinator.test.d.ts.map +1 -0
- package/build/supervisor/restart-coordinator.test.js +434 -0
- package/build/supervisor/restart-coordinator.test.js.map +1 -0
- package/build/test-support/fake-execute-tool-call.d.ts +11 -0
- package/build/test-support/fake-execute-tool-call.d.ts.map +1 -0
- package/build/test-support/fake-execute-tool-call.js +19 -0
- package/build/test-support/fake-execute-tool-call.js.map +1 -0
- package/build/tool-error.d.ts +14 -0
- package/build/tool-error.d.ts.map +1 -0
- package/build/tool-error.js +18 -0
- package/build/tool-error.js.map +1 -0
- package/build/tool-response.d.ts +293 -0
- package/build/tool-response.d.ts.map +1 -0
- package/build/tool-response.js +365 -0
- package/build/tool-response.js.map +1 -0
- package/build/tools/assert-tools.d.ts +62 -0
- package/build/tools/assert-tools.d.ts.map +1 -0
- package/build/tools/assert-tools.js +330 -0
- package/build/tools/assert-tools.js.map +1 -0
- package/build/tools/breakpoint-await-abort.test.d.ts +2 -0
- package/build/tools/breakpoint-await-abort.test.d.ts.map +1 -0
- package/build/tools/breakpoint-await-abort.test.js +124 -0
- package/build/tools/breakpoint-await-abort.test.js.map +1 -0
- package/build/tools/breakpoint-tools.d.ts +99 -0
- package/build/tools/breakpoint-tools.d.ts.map +1 -0
- package/build/tools/breakpoint-tools.js +1197 -0
- package/build/tools/breakpoint-tools.js.map +1 -0
- package/build/tools/config-tools.d.ts +58 -0
- package/build/tools/config-tools.d.ts.map +1 -0
- package/build/tools/config-tools.js +180 -0
- package/build/tools/config-tools.js.map +1 -0
- package/build/tools/connection-routing.test.d.ts +12 -0
- package/build/tools/connection-routing.test.d.ts.map +1 -0
- package/build/tools/connection-routing.test.js +206 -0
- package/build/tools/connection-routing.test.js.map +1 -0
- package/build/tools/console-tools.d.ts +84 -0
- package/build/tools/console-tools.d.ts.map +1 -0
- package/build/tools/console-tools.js +241 -0
- package/build/tools/console-tools.js.map +1 -0
- package/build/tools/content-tools.d.ts +74 -0
- package/build/tools/content-tools.d.ts.map +1 -0
- package/build/tools/content-tools.js +582 -0
- package/build/tools/content-tools.js.map +1 -0
- package/build/tools/dashboard-tools.d.ts +38 -0
- package/build/tools/dashboard-tools.d.ts.map +1 -0
- package/build/tools/dashboard-tools.js +123 -0
- package/build/tools/dashboard-tools.js.map +1 -0
- package/build/tools/dom-tools.d.ts +41 -0
- package/build/tools/dom-tools.d.ts.map +1 -0
- package/build/tools/dom-tools.js +277 -0
- package/build/tools/dom-tools.js.map +1 -0
- package/build/tools/download-tools.d.ts +55 -0
- package/build/tools/download-tools.d.ts.map +1 -0
- package/build/tools/download-tools.js +280 -0
- package/build/tools/download-tools.js.map +1 -0
- package/build/tools/execution-tools.d.ts +40 -0
- package/build/tools/execution-tools.d.ts.map +1 -0
- package/build/tools/execution-tools.js +121 -0
- package/build/tools/execution-tools.js.map +1 -0
- package/build/tools/input-abort.test.d.ts +2 -0
- package/build/tools/input-abort.test.d.ts.map +1 -0
- package/build/tools/input-abort.test.js +227 -0
- package/build/tools/input-abort.test.js.map +1 -0
- package/build/tools/input-ambiguous-selector.test.d.ts +2 -0
- package/build/tools/input-ambiguous-selector.test.d.ts.map +1 -0
- package/build/tools/input-ambiguous-selector.test.js +34 -0
- package/build/tools/input-ambiguous-selector.test.js.map +1 -0
- package/build/tools/input-tools.d.ts +168 -0
- package/build/tools/input-tools.d.ts.map +1 -0
- package/build/tools/input-tools.js +1397 -0
- package/build/tools/input-tools.js.map +1 -0
- package/build/tools/inspection-tools.d.ts +112 -0
- package/build/tools/inspection-tools.d.ts.map +1 -0
- package/build/tools/inspection-tools.js +637 -0
- package/build/tools/inspection-tools.js.map +1 -0
- package/build/tools/interpolation.d.ts +30 -0
- package/build/tools/interpolation.d.ts.map +1 -0
- package/build/tools/interpolation.js +143 -0
- package/build/tools/interpolation.js.map +1 -0
- package/build/tools/issues-abort-hygiene.test.d.ts +2 -0
- package/build/tools/issues-abort-hygiene.test.d.ts.map +1 -0
- package/build/tools/issues-abort-hygiene.test.js +79 -0
- package/build/tools/issues-abort-hygiene.test.js.map +1 -0
- package/build/tools/issues-tools.d.ts +98 -0
- package/build/tools/issues-tools.d.ts.map +1 -0
- package/build/tools/issues-tools.js +821 -0
- package/build/tools/issues-tools.js.map +1 -0
- package/build/tools/issues-tools.test.d.ts +8 -0
- package/build/tools/issues-tools.test.d.ts.map +1 -0
- package/build/tools/issues-tools.test.js +184 -0
- package/build/tools/issues-tools.test.js.map +1 -0
- package/build/tools/modal-tools.d.ts +73 -0
- package/build/tools/modal-tools.d.ts.map +1 -0
- package/build/tools/modal-tools.js +161 -0
- package/build/tools/modal-tools.js.map +1 -0
- package/build/tools/network-tools.d.ts +72 -0
- package/build/tools/network-tools.d.ts.map +1 -0
- package/build/tools/network-tools.js +432 -0
- package/build/tools/network-tools.js.map +1 -0
- package/build/tools/page-tools.d.ts +78 -0
- package/build/tools/page-tools.d.ts.map +1 -0
- package/build/tools/page-tools.js +255 -0
- package/build/tools/page-tools.js.map +1 -0
- package/build/tools/plugin-tools.d.ts +16 -0
- package/build/tools/plugin-tools.d.ts.map +1 -0
- package/build/tools/plugin-tools.js +17 -0
- package/build/tools/plugin-tools.js.map +1 -0
- package/build/tools/replay-abort.test.d.ts +2 -0
- package/build/tools/replay-abort.test.d.ts.map +1 -0
- package/build/tools/replay-abort.test.js +259 -0
- package/build/tools/replay-abort.test.js.map +1 -0
- package/build/tools/replay-add-conditional.test.d.ts +2 -0
- package/build/tools/replay-add-conditional.test.d.ts.map +1 -0
- package/build/tools/replay-add-conditional.test.js +205 -0
- package/build/tools/replay-add-conditional.test.js.map +1 -0
- package/build/tools/replay-background-run.test.d.ts +2 -0
- package/build/tools/replay-background-run.test.d.ts.map +1 -0
- package/build/tools/replay-background-run.test.js +176 -0
- package/build/tools/replay-background-run.test.js.map +1 -0
- package/build/tools/replay-capture-variables.test.d.ts +2 -0
- package/build/tools/replay-capture-variables.test.d.ts.map +1 -0
- package/build/tools/replay-capture-variables.test.js +331 -0
- package/build/tools/replay-capture-variables.test.js.map +1 -0
- package/build/tools/replay-conditional-idb-and-launch.test.d.ts +11 -0
- package/build/tools/replay-conditional-idb-and-launch.test.d.ts.map +1 -0
- package/build/tools/replay-conditional-idb-and-launch.test.js +429 -0
- package/build/tools/replay-conditional-idb-and-launch.test.js.map +1 -0
- package/build/tools/replay-conditional-timeout.test.d.ts +14 -0
- package/build/tools/replay-conditional-timeout.test.d.ts.map +1 -0
- package/build/tools/replay-conditional-timeout.test.js +126 -0
- package/build/tools/replay-conditional-timeout.test.js.map +1 -0
- package/build/tools/replay-declare.test.d.ts +2 -0
- package/build/tools/replay-declare.test.d.ts.map +1 -0
- package/build/tools/replay-declare.test.js +106 -0
- package/build/tools/replay-declare.test.js.map +1 -0
- package/build/tools/replay-declared-browser-cleanup.test.d.ts +2 -0
- package/build/tools/replay-declared-browser-cleanup.test.d.ts.map +1 -0
- package/build/tools/replay-declared-browser-cleanup.test.js +103 -0
- package/build/tools/replay-declared-browser-cleanup.test.js.map +1 -0
- package/build/tools/replay-declared-profile.test.d.ts +2 -0
- package/build/tools/replay-declared-profile.test.d.ts.map +1 -0
- package/build/tools/replay-declared-profile.test.js +116 -0
- package/build/tools/replay-declared-profile.test.js.map +1 -0
- package/build/tools/replay-executor.d.ts +556 -0
- package/build/tools/replay-executor.d.ts.map +1 -0
- package/build/tools/replay-executor.js +2347 -0
- package/build/tools/replay-executor.js.map +1 -0
- package/build/tools/replay-folder-save.test.d.ts +2 -0
- package/build/tools/replay-folder-save.test.d.ts.map +1 -0
- package/build/tools/replay-folder-save.test.js +63 -0
- package/build/tools/replay-folder-save.test.js.map +1 -0
- package/build/tools/replay-formatters.d.ts +148 -0
- package/build/tools/replay-formatters.d.ts.map +1 -0
- package/build/tools/replay-formatters.js +856 -0
- package/build/tools/replay-formatters.js.map +1 -0
- package/build/tools/replay-kill-chrome-on-finish.test.d.ts +2 -0
- package/build/tools/replay-kill-chrome-on-finish.test.d.ts.map +1 -0
- package/build/tools/replay-kill-chrome-on-finish.test.js +175 -0
- package/build/tools/replay-kill-chrome-on-finish.test.js.map +1 -0
- package/build/tools/replay-multi-connection.test.d.ts +2 -0
- package/build/tools/replay-multi-connection.test.d.ts.map +1 -0
- package/build/tools/replay-multi-connection.test.js +547 -0
- package/build/tools/replay-multi-connection.test.js.map +1 -0
- package/build/tools/replay-output-and-record-options.test.d.ts +2 -0
- package/build/tools/replay-output-and-record-options.test.d.ts.map +1 -0
- package/build/tools/replay-output-and-record-options.test.js +240 -0
- package/build/tools/replay-output-and-record-options.test.js.map +1 -0
- package/build/tools/replay-production-error-shape.test.d.ts +7 -0
- package/build/tools/replay-production-error-shape.test.d.ts.map +1 -0
- package/build/tools/replay-production-error-shape.test.js +93 -0
- package/build/tools/replay-production-error-shape.test.js.map +1 -0
- package/build/tools/replay-rebase.test.d.ts +2 -0
- package/build/tools/replay-rebase.test.d.ts.map +1 -0
- package/build/tools/replay-rebase.test.js +58 -0
- package/build/tools/replay-rebase.test.js.map +1 -0
- package/build/tools/replay-retry-classification.test.d.ts +9 -0
- package/build/tools/replay-retry-classification.test.d.ts.map +1 -0
- package/build/tools/replay-retry-classification.test.js +57 -0
- package/build/tools/replay-retry-classification.test.js.map +1 -0
- package/build/tools/replay-run-registry.d.ts +55 -0
- package/build/tools/replay-run-registry.d.ts.map +1 -0
- package/build/tools/replay-run-registry.js +65 -0
- package/build/tools/replay-run-registry.js.map +1 -0
- package/build/tools/replay-sequence-reload.test.d.ts +2 -0
- package/build/tools/replay-sequence-reload.test.d.ts.map +1 -0
- package/build/tools/replay-sequence-reload.test.js +82 -0
- package/build/tools/replay-sequence-reload.test.js.map +1 -0
- package/build/tools/replay-socket-scope.test.d.ts +2 -0
- package/build/tools/replay-socket-scope.test.d.ts.map +1 -0
- package/build/tools/replay-socket-scope.test.js +75 -0
- package/build/tools/replay-socket-scope.test.js.map +1 -0
- package/build/tools/replay-step-connection.test.d.ts +2 -0
- package/build/tools/replay-step-connection.test.d.ts.map +1 -0
- package/build/tools/replay-step-connection.test.js +292 -0
- package/build/tools/replay-step-connection.test.js.map +1 -0
- package/build/tools/replay-step-timeout.test.d.ts +2 -0
- package/build/tools/replay-step-timeout.test.d.ts.map +1 -0
- package/build/tools/replay-step-timeout.test.js +204 -0
- package/build/tools/replay-step-timeout.test.js.map +1 -0
- package/build/tools/replay-tags.test.d.ts +2 -0
- package/build/tools/replay-tags.test.d.ts.map +1 -0
- package/build/tools/replay-tags.test.js +99 -0
- package/build/tools/replay-tags.test.js.map +1 -0
- package/build/tools/replay-teardown-and-foreach.test.d.ts +13 -0
- package/build/tools/replay-teardown-and-foreach.test.d.ts.map +1 -0
- package/build/tools/replay-teardown-and-foreach.test.js +318 -0
- package/build/tools/replay-teardown-and-foreach.test.js.map +1 -0
- package/build/tools/replay-tool-name-validation.test.d.ts +2 -0
- package/build/tools/replay-tool-name-validation.test.d.ts.map +1 -0
- package/build/tools/replay-tool-name-validation.test.js +280 -0
- package/build/tools/replay-tool-name-validation.test.js.map +1 -0
- package/build/tools/replay-tools.d.ts +288 -0
- package/build/tools/replay-tools.d.ts.map +1 -0
- package/build/tools/replay-tools.js +2994 -0
- package/build/tools/replay-tools.js.map +1 -0
- package/build/tools/request-abort.test.d.ts +2 -0
- package/build/tools/request-abort.test.d.ts.map +1 -0
- package/build/tools/request-abort.test.js +130 -0
- package/build/tools/request-abort.test.js.map +1 -0
- package/build/tools/request-tools.d.ts +56 -0
- package/build/tools/request-tools.d.ts.map +1 -0
- package/build/tools/request-tools.js +174 -0
- package/build/tools/request-tools.js.map +1 -0
- package/build/tools/screenshot-tools.d.ts +135 -0
- package/build/tools/screenshot-tools.d.ts.map +1 -0
- package/build/tools/screenshot-tools.js +636 -0
- package/build/tools/screenshot-tools.js.map +1 -0
- package/build/tools/server-tools.d.ts +88 -0
- package/build/tools/server-tools.d.ts.map +1 -0
- package/build/tools/server-tools.js +485 -0
- package/build/tools/server-tools.js.map +1 -0
- package/build/tools/source-tools.d.ts +46 -0
- package/build/tools/source-tools.d.ts.map +1 -0
- package/build/tools/source-tools.js +47 -0
- package/build/tools/source-tools.js.map +1 -0
- package/build/tools/storage-tools.d.ts +141 -0
- package/build/tools/storage-tools.d.ts.map +1 -0
- package/build/tools/storage-tools.js +894 -0
- package/build/tools/storage-tools.js.map +1 -0
- package/build/tools/storage-tools.test.d.ts +13 -0
- package/build/tools/storage-tools.test.d.ts.map +1 -0
- package/build/tools/storage-tools.test.js +707 -0
- package/build/tools/storage-tools.test.js.map +1 -0
- package/build/tools/tab-tools.d.ts +42 -0
- package/build/tools/tab-tools.d.ts.map +1 -0
- package/build/tools/tab-tools.js +296 -0
- package/build/tools/tab-tools.js.map +1 -0
- package/build/tools/wait-tools.d.ts +85 -0
- package/build/tools/wait-tools.d.ts.map +1 -0
- package/build/tools/wait-tools.js +236 -0
- package/build/tools/wait-tools.js.map +1 -0
- package/build/tools/wait-tools.test.d.ts +9 -0
- package/build/tools/wait-tools.test.d.ts.map +1 -0
- package/build/tools/wait-tools.test.js +193 -0
- package/build/tools/wait-tools.test.js.map +1 -0
- package/build/types.d.ts +133 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +5 -0
- package/build/types.js.map +1 -0
- package/build/ui-verifier.d.ts +88 -0
- package/build/ui-verifier.d.ts.map +1 -0
- package/build/ui-verifier.js +540 -0
- package/build/ui-verifier.js.map +1 -0
- package/build/utils/abort.d.ts +68 -0
- package/build/utils/abort.d.ts.map +1 -0
- package/build/utils/abort.js +159 -0
- package/build/utils/abort.js.map +1 -0
- package/build/utils/abort.test.d.ts +2 -0
- package/build/utils/abort.test.d.ts.map +1 -0
- package/build/utils/abort.test.js +174 -0
- package/build/utils/abort.test.js.map +1 -0
- package/build/utils/modal-detection-core.d.ts +150 -0
- package/build/utils/modal-detection-core.d.ts.map +1 -0
- package/build/utils/modal-detection-core.js +332 -0
- package/build/utils/modal-detection-core.js.map +1 -0
- package/build/utils/modal-detection-core.test.d.ts +5 -0
- package/build/utils/modal-detection-core.test.d.ts.map +1 -0
- package/build/utils/modal-detection-core.test.js +492 -0
- package/build/utils/modal-detection-core.test.js.map +1 -0
- package/build/utils/modal-detector.d.ts +29 -0
- package/build/utils/modal-detector.d.ts.map +1 -0
- package/build/utils/modal-detector.js +281 -0
- package/build/utils/modal-detector.js.map +1 -0
- package/build/utils/modal-dismissal.d.ts +32 -0
- package/build/utils/modal-dismissal.d.ts.map +1 -0
- package/build/utils/modal-dismissal.js +190 -0
- package/build/utils/modal-dismissal.js.map +1 -0
- package/build/utils/port-check.d.ts +31 -0
- package/build/utils/port-check.d.ts.map +1 -0
- package/build/utils/port-check.js +87 -0
- package/build/utils/port-check.js.map +1 -0
- package/build/utils/port-check.test.d.ts +2 -0
- package/build/utils/port-check.test.d.ts.map +1 -0
- package/build/utils/port-check.test.js +66 -0
- package/build/utils/port-check.test.js.map +1 -0
- package/build/utils/selector-resolver.d.ts +66 -0
- package/build/utils/selector-resolver.d.ts.map +1 -0
- package/build/utils/selector-resolver.js +200 -0
- package/build/utils/selector-resolver.js.map +1 -0
- package/build/validation-helpers.d.ts +41 -0
- package/build/validation-helpers.d.ts.map +1 -0
- package/build/validation-helpers.js +236 -0
- package/build/validation-helpers.js.map +1 -0
- package/build/window-sizing.d.ts +26 -0
- package/build/window-sizing.d.ts.map +1 -0
- package/build/window-sizing.js +87 -0
- package/build/window-sizing.js.map +1 -0
- package/build/window-sizing.test.d.ts +8 -0
- package/build/window-sizing.test.d.ts.map +1 -0
- package/build/window-sizing.test.js +113 -0
- package/build/window-sizing.test.js.map +1 -0
- package/docs/README.md +74 -0
- package/docs/automation.md +562 -0
- package/docs/debugging.md +229 -0
- package/docs/installation.md +133 -0
- package/docs/instructions.md +227 -0
- package/docs/mcp-instructions.md +15 -0
- package/docs/message-system.md +327 -0
- package/docs/messages.md +3480 -0
- package/docs/parser-plugins.md +132 -0
- package/docs/replay.md +1411 -0
- package/docs/troubleshooting.md +272 -0
- package/docs/working_with_llm_notes.md +5 -0
- package/package.json +87 -0
- package/skills/devharness/SKILL.md +195 -0
- package/skills/devharness/references/sequences.md +613 -0
- package/skills/devharness/references/tool-categories.md +99 -0
|
@@ -0,0 +1,1828 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP Connection Manager
|
|
3
|
+
* Handles connection to Chrome DevTools Protocol (Chrome or Node.js)
|
|
4
|
+
*/
|
|
5
|
+
import CDP from 'chrome-remote-interface';
|
|
6
|
+
import { debugLog } from './debug-logger.js';
|
|
7
|
+
/**
|
|
8
|
+
* Thrown by evaluateExpression() when the evaluated code itself threw
|
|
9
|
+
* (CDP reports this via `exceptionDetails` on an otherwise-successful
|
|
10
|
+
* Runtime.evaluate/Debugger.evaluateOnCallFrame response, NOT as a rejected
|
|
11
|
+
* promise) - e.g. `Math.max(...bigArray)` throwing a stack-exhaustion
|
|
12
|
+
* RangeError. This is an ordinary outcome of evaluating arbitrary user code
|
|
13
|
+
* and must be surfaced as a normal tool error rather than silently ignored.
|
|
14
|
+
*/
|
|
15
|
+
export class EvaluateExpressionExceptionError extends Error {
|
|
16
|
+
expression;
|
|
17
|
+
exceptionType;
|
|
18
|
+
exceptionMessage;
|
|
19
|
+
exceptionStack;
|
|
20
|
+
constructor(expression, exceptionType, exceptionMessage, exceptionStack) {
|
|
21
|
+
super(`${exceptionType}: ${exceptionMessage}`);
|
|
22
|
+
this.expression = expression;
|
|
23
|
+
this.exceptionType = exceptionType;
|
|
24
|
+
this.exceptionMessage = exceptionMessage;
|
|
25
|
+
this.exceptionStack = exceptionStack;
|
|
26
|
+
this.name = 'EvaluateExpressionExceptionError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Thrown by evaluateExpression() when the underlying CDP call does not
|
|
31
|
+
* return within the bounded client-side timeout. This is the backstop for
|
|
32
|
+
* cases where the renderer/execution context stops responding entirely (no
|
|
33
|
+
* result, no exception, no CDP-level timeout) - see the RangeError
|
|
34
|
+
* stack-exhaustion hang this was written to fix.
|
|
35
|
+
*/
|
|
36
|
+
export class EvaluateExpressionTimeoutError extends Error {
|
|
37
|
+
expression;
|
|
38
|
+
timeoutMs;
|
|
39
|
+
constructor(expression, timeoutMs) {
|
|
40
|
+
super(`Evaluation timed out after ${timeoutMs}ms - the execution context may be unresponsive`);
|
|
41
|
+
this.expression = expression;
|
|
42
|
+
this.timeoutMs = timeoutMs;
|
|
43
|
+
this.name = 'EvaluateExpressionTimeoutError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Thrown by evaluateExpression() when the expression returned a Promise that
|
|
48
|
+
* is still pending while the debugger is paused. The paused event loop can
|
|
49
|
+
* never run the microtask queue, so the promise can never settle - awaiting
|
|
50
|
+
* it would only burn the full client-side timeout. Failing fast with an
|
|
51
|
+
* explanation is strictly better. (Promises that already settled - e.g.
|
|
52
|
+
* `Promise.resolve(x)` or an async IIFE with no real await - ARE recovered
|
|
53
|
+
* via their [[PromiseState]]/[[PromiseResult]] internal properties and never
|
|
54
|
+
* reach this error.)
|
|
55
|
+
*/
|
|
56
|
+
export class EvaluateExpressionPendingPromiseError extends Error {
|
|
57
|
+
expression;
|
|
58
|
+
constructor(expression) {
|
|
59
|
+
super('Expression returned a pending Promise while the debugger is paused - the event loop is stopped, so it can never settle');
|
|
60
|
+
this.expression = expression;
|
|
61
|
+
this.name = 'EvaluateExpressionPendingPromiseError';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Truncate a long expression for inclusion in error messages/telemetry. */
|
|
65
|
+
function truncateExpression(expression, maxLength = 200) {
|
|
66
|
+
return expression.length > maxLength ? `${expression.slice(0, maxLength)}...` : expression;
|
|
67
|
+
}
|
|
68
|
+
export class CDPManager {
|
|
69
|
+
client = null;
|
|
70
|
+
state = {
|
|
71
|
+
connected: false,
|
|
72
|
+
paused: false,
|
|
73
|
+
breakpoints: new Map(),
|
|
74
|
+
};
|
|
75
|
+
scriptIdToUrl = new Map();
|
|
76
|
+
urlToScriptId = new Map(); // Support multiple scripts per URL (inline HTML scripts)
|
|
77
|
+
pauseResolvers = [];
|
|
78
|
+
scriptWaitResolvers = [];
|
|
79
|
+
sourceMapHandler = null;
|
|
80
|
+
logpointLimitExceeded = null;
|
|
81
|
+
consoleMessageCallback = null;
|
|
82
|
+
pauseCallback = null;
|
|
83
|
+
resumeCallback = null;
|
|
84
|
+
// DOMDebugger state for advanced breakpoints
|
|
85
|
+
domBreakpoints = new Map();
|
|
86
|
+
eventBreakpoints = new Map();
|
|
87
|
+
xhrBreakpoints = new Map();
|
|
88
|
+
advancedBpCounter = 0;
|
|
89
|
+
constructor(sourceMapHandler) {
|
|
90
|
+
this.sourceMapHandler = sourceMapHandler || null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Set a callback to receive console messages from Runtime.consoleAPICalled
|
|
94
|
+
* This is used for Node.js debugging where Puppeteer's page.on('console') isn't available
|
|
95
|
+
*/
|
|
96
|
+
setConsoleMessageCallback(callback) {
|
|
97
|
+
this.consoleMessageCallback = callback;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Set a callback to be called when debugger pauses at a breakpoint
|
|
101
|
+
*/
|
|
102
|
+
setPauseCallback(callback) {
|
|
103
|
+
this.pauseCallback = callback;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Set a callback to be called when debugger resumes from a breakpoint
|
|
107
|
+
*/
|
|
108
|
+
setResumeCallback(callback) {
|
|
109
|
+
this.resumeCallback = callback;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Find script IDs by URL, with fallback matching strategies:
|
|
113
|
+
* 1. Exact match
|
|
114
|
+
* 2. Base URL match (strips query params)
|
|
115
|
+
* 3. Filename/path suffix match (e.g., "click.js" matches "/controls/click.js")
|
|
116
|
+
*
|
|
117
|
+
* This allows breakpoints to work with partial URLs and across page reloads
|
|
118
|
+
* Returns the most recently loaded script (highest scriptId) when multiple matches exist
|
|
119
|
+
*/
|
|
120
|
+
findScriptIds(url) {
|
|
121
|
+
// First, try exact match
|
|
122
|
+
const exactMatch = this.urlToScriptId.get(url);
|
|
123
|
+
if (exactMatch && exactMatch.length > 0) {
|
|
124
|
+
return { scriptIds: exactMatch, matchedUrl: url };
|
|
125
|
+
}
|
|
126
|
+
// Second, try matching by base URL (strip query params)
|
|
127
|
+
// If multiple matches, prefer the one with the highest scriptId (most recent)
|
|
128
|
+
const baseUrl = url.split('?')[0];
|
|
129
|
+
let bestMatch = null;
|
|
130
|
+
let highestScriptId = -1;
|
|
131
|
+
for (const [loadedUrl, scriptIds] of this.urlToScriptId.entries()) {
|
|
132
|
+
const loadedBaseUrl = loadedUrl.split('?')[0];
|
|
133
|
+
if (loadedBaseUrl === baseUrl && scriptIds.length > 0) {
|
|
134
|
+
// Get the highest numeric scriptId from this entry
|
|
135
|
+
const maxId = Math.max(...scriptIds.map(id => parseInt(id, 10) || 0));
|
|
136
|
+
if (maxId > highestScriptId) {
|
|
137
|
+
highestScriptId = maxId;
|
|
138
|
+
bestMatch = { scriptIds, matchedUrl: loadedUrl };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (bestMatch) {
|
|
143
|
+
return bestMatch;
|
|
144
|
+
}
|
|
145
|
+
// Third, try filename/path suffix match
|
|
146
|
+
// This handles cases like "click.js" matching "http://localhost/controls/click.js"
|
|
147
|
+
// or "controls/click.js" matching "http://localhost/controls/click.js"
|
|
148
|
+
for (const [loadedUrl, scriptIds] of this.urlToScriptId.entries()) {
|
|
149
|
+
const loadedBaseUrl = loadedUrl.split('?')[0];
|
|
150
|
+
// Check if the loaded URL ends with the provided URL (after stripping protocol/host)
|
|
151
|
+
if (loadedBaseUrl.endsWith('/' + baseUrl) || loadedBaseUrl.endsWith(baseUrl)) {
|
|
152
|
+
if (scriptIds.length > 0) {
|
|
153
|
+
const maxId = Math.max(...scriptIds.map(id => parseInt(id, 10) || 0));
|
|
154
|
+
if (maxId > highestScriptId) {
|
|
155
|
+
highestScriptId = maxId;
|
|
156
|
+
bestMatch = { scriptIds, matchedUrl: loadedUrl };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return bestMatch;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Check if a script with the given URL (or matching suffix) is loaded
|
|
165
|
+
* This is useful to detect Vite-style serving where .ts files are loaded directly
|
|
166
|
+
*/
|
|
167
|
+
isScriptLoaded(url) {
|
|
168
|
+
return this.findScriptIds(url) !== null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Get the URL for a script by its ID
|
|
172
|
+
*/
|
|
173
|
+
getScriptUrl(scriptId) {
|
|
174
|
+
return this.scriptIdToUrl.get(scriptId);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Connect to a Chrome or Node.js debugger instance
|
|
178
|
+
* @param host - The debugger host (default: localhost)
|
|
179
|
+
* @param port - The debugger port (default: 9222)
|
|
180
|
+
* @param targetId - Optional target ID to connect to a specific page/tab
|
|
181
|
+
*/
|
|
182
|
+
async connect(host = 'localhost', port = 9222, targetId) {
|
|
183
|
+
try {
|
|
184
|
+
// If targetId is provided, connect to that specific target
|
|
185
|
+
// Otherwise, connect to the default target
|
|
186
|
+
const cdpOptions = { host, port };
|
|
187
|
+
if (targetId) {
|
|
188
|
+
cdpOptions.target = targetId;
|
|
189
|
+
}
|
|
190
|
+
this.client = await CDP(cdpOptions);
|
|
191
|
+
const { Debugger, Runtime, DOM } = this.client;
|
|
192
|
+
// Set up event listeners BEFORE enabling domains
|
|
193
|
+
// This ensures we capture scriptParsed events for already-loaded scripts
|
|
194
|
+
// (which are emitted immediately when Debugger.enable() is called)
|
|
195
|
+
Debugger.scriptParsed((params) => {
|
|
196
|
+
this.scriptIdToUrl.set(params.scriptId, params.url);
|
|
197
|
+
// Support multiple scripts per URL (inline HTML script blocks)
|
|
198
|
+
const existingScripts = this.urlToScriptId.get(params.url) || [];
|
|
199
|
+
this.urlToScriptId.set(params.url, [...existingScripts, params.scriptId]);
|
|
200
|
+
// Register source map for lazy loading (loaded on-demand when needed)
|
|
201
|
+
if (params.sourceMapURL && this.sourceMapHandler) {
|
|
202
|
+
this.sourceMapHandler.registerSourceMap(params.url, params.sourceMapURL);
|
|
203
|
+
}
|
|
204
|
+
// Check if any waitForScript calls are waiting for this script
|
|
205
|
+
const matchingResolvers = [];
|
|
206
|
+
this.scriptWaitResolvers.forEach((waiter, index) => {
|
|
207
|
+
const matches = typeof waiter.pattern === 'string'
|
|
208
|
+
? params.url.includes(waiter.pattern)
|
|
209
|
+
: waiter.pattern.test(params.url);
|
|
210
|
+
if (matches) {
|
|
211
|
+
waiter.resolve(params.url);
|
|
212
|
+
matchingResolvers.push(index);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
// Remove matched resolvers (in reverse order to maintain indices)
|
|
216
|
+
matchingResolvers.reverse().forEach(index => {
|
|
217
|
+
this.scriptWaitResolvers.splice(index, 1);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
Debugger.paused((params) => {
|
|
221
|
+
debugLog('cdp-manager', `Debugger.paused event received, resolvers count: ${this.pauseResolvers.length}`);
|
|
222
|
+
this.state.paused = true;
|
|
223
|
+
this.state.currentCallFrames = params.callFrames;
|
|
224
|
+
// Resolve all pending pause promises
|
|
225
|
+
const resolvers = this.pauseResolvers.splice(0);
|
|
226
|
+
debugLog('cdp-manager', `Resolving ${resolvers.length} pause resolvers`);
|
|
227
|
+
resolvers.forEach(resolve => resolve());
|
|
228
|
+
// Inject clickable console link when paused at breakpoint
|
|
229
|
+
if (params.callFrames && params.callFrames.length > 0) {
|
|
230
|
+
const location = params.callFrames[0].location;
|
|
231
|
+
const url = this.scriptIdToUrl.get(location.scriptId) || 'unknown';
|
|
232
|
+
this.injectConsoleLink(url, location.lineNumber, '⏸️ Paused at');
|
|
233
|
+
}
|
|
234
|
+
// Notify pause callback (e.g., to pause port monitoring)
|
|
235
|
+
if (this.pauseCallback) {
|
|
236
|
+
this.pauseCallback();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
Debugger.resumed(() => {
|
|
240
|
+
this.state.paused = false;
|
|
241
|
+
this.state.currentCallFrames = undefined;
|
|
242
|
+
// Notify resume callback (e.g., to resume port monitoring)
|
|
243
|
+
if (this.resumeCallback) {
|
|
244
|
+
this.resumeCallback();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
// Listen for breakpoint resolution - updates pending breakpoints when script loads
|
|
248
|
+
Debugger.breakpointResolved((params) => {
|
|
249
|
+
const { breakpointId, location } = params;
|
|
250
|
+
const existingBp = this.state.breakpoints.get(breakpointId);
|
|
251
|
+
if (existingBp && existingBp.status === 'pending') {
|
|
252
|
+
// Update the breakpoint from pending to resolved
|
|
253
|
+
existingBp.status = 'resolved';
|
|
254
|
+
existingBp.location = location;
|
|
255
|
+
this.state.breakpoints.set(breakpointId, existingBp);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
// Listen for console messages via CDP (works for both Chrome and Node.js)
|
|
259
|
+
// This enables console capture for Node.js where Puppeteer isn't available
|
|
260
|
+
Runtime.consoleAPICalled((params) => {
|
|
261
|
+
if (this.consoleMessageCallback) {
|
|
262
|
+
this.consoleMessageCallback(params);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
// Enable domains AFTER setting up event listeners
|
|
266
|
+
// This ensures we capture all events including scriptParsed for already-loaded scripts
|
|
267
|
+
await Debugger.enable();
|
|
268
|
+
await Runtime.enable();
|
|
269
|
+
// Enable DOM domain (required for DOMDebugger breakpoints to work)
|
|
270
|
+
// Note: DOM domain is not available in Node.js, only in browsers
|
|
271
|
+
try {
|
|
272
|
+
await DOM.enable();
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// DOM domain not available (e.g., Node.js runtime)
|
|
276
|
+
}
|
|
277
|
+
// Detect runtime type
|
|
278
|
+
this.state.runtimeType = await this.detectRuntimeType();
|
|
279
|
+
this.state.connected = true;
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
throw new Error(`Failed to connect to debugger: ${error}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Disconnect from the debugger
|
|
287
|
+
*/
|
|
288
|
+
async disconnect() {
|
|
289
|
+
if (this.client) {
|
|
290
|
+
const wasPaused = this.state.paused;
|
|
291
|
+
try {
|
|
292
|
+
await this.client.close();
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
this.client = null;
|
|
296
|
+
this.state.connected = false;
|
|
297
|
+
this.state.paused = false;
|
|
298
|
+
this.state.currentCallFrames = undefined;
|
|
299
|
+
this.state.runtimeType = undefined;
|
|
300
|
+
this.state.breakpoints.clear();
|
|
301
|
+
this.scriptIdToUrl.clear();
|
|
302
|
+
this.urlToScriptId.clear();
|
|
303
|
+
// Clear DOMDebugger state
|
|
304
|
+
this.domBreakpoints.clear();
|
|
305
|
+
this.eventBreakpoints.clear();
|
|
306
|
+
this.xhrBreakpoints.clear();
|
|
307
|
+
// A connection torn down while paused would otherwise never fire
|
|
308
|
+
// Debugger.resumed, leaving anything gated on it (e.g. port monitoring)
|
|
309
|
+
// stuck paused forever.
|
|
310
|
+
if (wasPaused && this.resumeCallback) {
|
|
311
|
+
this.resumeCallback();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Check if connected
|
|
318
|
+
*/
|
|
319
|
+
isConnected() {
|
|
320
|
+
return this.state.connected;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Check if currently paused at a breakpoint
|
|
324
|
+
*/
|
|
325
|
+
isPaused() {
|
|
326
|
+
return this.state.paused;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Get the runtime type (chrome, node, or unknown)
|
|
330
|
+
*/
|
|
331
|
+
getRuntimeType() {
|
|
332
|
+
return this.state.runtimeType || 'unknown';
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Detect whether we're connected to Chrome/browser or Node.js
|
|
336
|
+
* This detection happens by checking for browser-specific global objects
|
|
337
|
+
*/
|
|
338
|
+
async detectRuntimeType() {
|
|
339
|
+
if (!this.client) {
|
|
340
|
+
return 'unknown';
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const { Runtime } = this.client;
|
|
344
|
+
// Try to evaluate 'typeof window' - exists in browsers, not in Node.js
|
|
345
|
+
const windowCheck = await Runtime.evaluate({
|
|
346
|
+
expression: 'typeof window',
|
|
347
|
+
silent: true,
|
|
348
|
+
});
|
|
349
|
+
// If 'window' is defined, we're in a browser (Chrome)
|
|
350
|
+
if (windowCheck.result.type === 'object' ||
|
|
351
|
+
windowCheck.result.value === 'object') {
|
|
352
|
+
return 'chrome';
|
|
353
|
+
}
|
|
354
|
+
// Try to evaluate 'typeof process' - exists in Node.js, not in browsers
|
|
355
|
+
const processCheck = await Runtime.evaluate({
|
|
356
|
+
expression: 'typeof process',
|
|
357
|
+
silent: true,
|
|
358
|
+
});
|
|
359
|
+
// If 'process' is an object, we're in Node.js
|
|
360
|
+
if (processCheck.result.type === 'object' ||
|
|
361
|
+
processCheck.result.value === 'object') {
|
|
362
|
+
return 'node';
|
|
363
|
+
}
|
|
364
|
+
return 'unknown';
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
console.error('Failed to detect runtime type:', error);
|
|
368
|
+
return 'unknown';
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Set a breakpoint at a specific file and line
|
|
373
|
+
*/
|
|
374
|
+
async setBreakpoint(url, lineNumber, columnNumber, condition) {
|
|
375
|
+
if (!this.state.connected) {
|
|
376
|
+
throw new Error('Not connected to debugger');
|
|
377
|
+
}
|
|
378
|
+
const { Debugger } = this.client;
|
|
379
|
+
// IMPORTANT: CDP uses 0-based line and column numbers internally
|
|
380
|
+
// User provides 1-based numbers (line 1 = first line, column 1 = first column)
|
|
381
|
+
// We must convert before calling CDP API
|
|
382
|
+
const cdpLineNumber = lineNumber - 1; // Convert 1-based → 0-based
|
|
383
|
+
const cdpColumnNumber = columnNumber !== undefined ? columnNumber - 1 : undefined; // Convert 1-based → 0-based
|
|
384
|
+
// Try to find actual URL if the provided one has query params that may differ
|
|
385
|
+
// This handles cache-busting params like ?v=123 that change on rebuild
|
|
386
|
+
let actualUrl = url;
|
|
387
|
+
const match = this.findScriptIds(url);
|
|
388
|
+
if (match && match.matchedUrl !== url) {
|
|
389
|
+
actualUrl = match.matchedUrl;
|
|
390
|
+
}
|
|
391
|
+
// Set breakpoint using 0-based CDP numbers
|
|
392
|
+
let result;
|
|
393
|
+
try {
|
|
394
|
+
result = await Debugger.setBreakpointByUrl({
|
|
395
|
+
url: actualUrl,
|
|
396
|
+
lineNumber: cdpLineNumber,
|
|
397
|
+
columnNumber: cdpColumnNumber,
|
|
398
|
+
condition,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
// Handle "Breakpoint at specified location already exists" error
|
|
403
|
+
if (error?.message?.includes('already exists')) {
|
|
404
|
+
// Find and remove the existing breakpoint at this location
|
|
405
|
+
for (const [bpId, bp] of this.state.breakpoints.entries()) {
|
|
406
|
+
if (bp.originalLocation?.url === url &&
|
|
407
|
+
bp.originalLocation?.lineNumber === lineNumber &&
|
|
408
|
+
(columnNumber === undefined || bp.originalLocation?.columnNumber === columnNumber)) {
|
|
409
|
+
await this.removeBreakpoint(bpId);
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Retry setting the breakpoint
|
|
414
|
+
result = await Debugger.setBreakpointByUrl({
|
|
415
|
+
url: actualUrl,
|
|
416
|
+
lineNumber: cdpLineNumber,
|
|
417
|
+
columnNumber: cdpColumnNumber,
|
|
418
|
+
condition,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Check if breakpoint was resolved to any location
|
|
426
|
+
// Per CDP spec, setBreakpointByUrl can return empty locations if script isn't loaded yet
|
|
427
|
+
// The breakpoint is still valid and will activate when the script loads
|
|
428
|
+
if (!result.locations || result.locations.length === 0) {
|
|
429
|
+
// Breakpoint is pending - script not loaded yet but breakpoint is queued
|
|
430
|
+
const breakpointInfo = {
|
|
431
|
+
breakpointId: result.breakpointId,
|
|
432
|
+
// For pending breakpoints, we don't have a resolved location yet
|
|
433
|
+
// Use a placeholder that indicates pending state
|
|
434
|
+
location: {
|
|
435
|
+
scriptId: '',
|
|
436
|
+
lineNumber: cdpLineNumber, // Store the requested 0-based line
|
|
437
|
+
columnNumber: cdpColumnNumber,
|
|
438
|
+
},
|
|
439
|
+
originalLocation: { url, lineNumber, columnNumber },
|
|
440
|
+
status: 'pending',
|
|
441
|
+
};
|
|
442
|
+
this.state.breakpoints.set(result.breakpointId, breakpointInfo);
|
|
443
|
+
return breakpointInfo;
|
|
444
|
+
}
|
|
445
|
+
// Warn if multiple locations (rare but possible)
|
|
446
|
+
if (result.locations.length > 1) {
|
|
447
|
+
console.error(`[cdp-tools] Warning: Breakpoint matched ${result.locations.length} locations. Using first match.`);
|
|
448
|
+
}
|
|
449
|
+
// Store breakpoint info
|
|
450
|
+
// - location: Actual location from CDP (0-based)
|
|
451
|
+
// - originalLocation: User-requested location (1-based, what user asked for)
|
|
452
|
+
const breakpointInfo = {
|
|
453
|
+
breakpointId: result.breakpointId,
|
|
454
|
+
location: result.locations[0],
|
|
455
|
+
originalLocation: { url, lineNumber, columnNumber },
|
|
456
|
+
status: 'resolved',
|
|
457
|
+
};
|
|
458
|
+
this.state.breakpoints.set(result.breakpointId, breakpointInfo);
|
|
459
|
+
return breakpointInfo;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Remove a breakpoint
|
|
463
|
+
*/
|
|
464
|
+
async removeBreakpoint(breakpointId) {
|
|
465
|
+
if (!this.state.connected) {
|
|
466
|
+
throw new Error('Not connected to debugger');
|
|
467
|
+
}
|
|
468
|
+
const { Debugger } = this.client;
|
|
469
|
+
await Debugger.removeBreakpoint({ breakpointId });
|
|
470
|
+
this.state.breakpoints.delete(breakpointId);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get all active breakpoints
|
|
474
|
+
*/
|
|
475
|
+
getBreakpoints() {
|
|
476
|
+
return Array.from(this.state.breakpoints.values());
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Get breakpoint counts (total, regular, logpoints)
|
|
480
|
+
*/
|
|
481
|
+
getBreakpointCounts() {
|
|
482
|
+
const all = this.getBreakpoints();
|
|
483
|
+
const logpoints = all.filter(bp => bp.isLogpoint).length;
|
|
484
|
+
const regularBreakpoints = all.length - logpoints;
|
|
485
|
+
return {
|
|
486
|
+
total: all.length,
|
|
487
|
+
breakpoints: regularBreakpoints,
|
|
488
|
+
logpoints,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Wait for a script matching the given URL pattern to load
|
|
493
|
+
* @param urlPattern - String (substring match) or RegExp to match against script URLs
|
|
494
|
+
* @param timeout - Maximum time to wait in milliseconds (default: 10000)
|
|
495
|
+
* @returns The URL of the matched script
|
|
496
|
+
* @throws Error if timeout expires before script loads
|
|
497
|
+
*/
|
|
498
|
+
async waitForScript(urlPattern, timeout = 10000) {
|
|
499
|
+
if (!this.state.connected) {
|
|
500
|
+
throw new Error('Not connected to debugger');
|
|
501
|
+
}
|
|
502
|
+
// First, check if script is already loaded
|
|
503
|
+
for (const url of this.urlToScriptId.keys()) {
|
|
504
|
+
const matches = typeof urlPattern === 'string'
|
|
505
|
+
? url.includes(urlPattern)
|
|
506
|
+
: urlPattern.test(url);
|
|
507
|
+
if (matches) {
|
|
508
|
+
return url; // Script already loaded
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// Script not loaded yet, wait for it
|
|
512
|
+
return new Promise((resolve, reject) => {
|
|
513
|
+
const timeoutId = setTimeout(() => {
|
|
514
|
+
// Remove this waiter from the list
|
|
515
|
+
const index = this.scriptWaitResolvers.findIndex(w => w.resolve === resolveWrapper);
|
|
516
|
+
if (index >= 0) {
|
|
517
|
+
this.scriptWaitResolvers.splice(index, 1);
|
|
518
|
+
}
|
|
519
|
+
reject(new Error(`Timeout: Script matching "${urlPattern}" did not load within ${timeout}ms`));
|
|
520
|
+
}, timeout);
|
|
521
|
+
const resolveWrapper = (url) => {
|
|
522
|
+
clearTimeout(timeoutId);
|
|
523
|
+
resolve(url);
|
|
524
|
+
};
|
|
525
|
+
this.scriptWaitResolvers.push({
|
|
526
|
+
pattern: urlPattern,
|
|
527
|
+
resolve: resolveWrapper,
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Get list of all loaded scripts
|
|
533
|
+
* @returns Array of script URLs
|
|
534
|
+
*/
|
|
535
|
+
getLoadedScripts() {
|
|
536
|
+
return Array.from(this.urlToScriptId.keys());
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Check if a script matching the pattern is loaded
|
|
540
|
+
* @param urlPattern - String (substring match) or RegExp
|
|
541
|
+
* @returns The matched URL or null
|
|
542
|
+
*/
|
|
543
|
+
findLoadedScript(urlPattern) {
|
|
544
|
+
for (const url of this.urlToScriptId.keys()) {
|
|
545
|
+
const matches = typeof urlPattern === 'string'
|
|
546
|
+
? url.includes(urlPattern)
|
|
547
|
+
: urlPattern.test(url);
|
|
548
|
+
if (matches) {
|
|
549
|
+
return url;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Synchronize breakpoint state with CDP's actual breakpoints
|
|
556
|
+
* Use this to recover from state desynchronization
|
|
557
|
+
*/
|
|
558
|
+
async syncBreakpoints() {
|
|
559
|
+
if (!this.state.connected) {
|
|
560
|
+
throw new Error('Not connected to debugger');
|
|
561
|
+
}
|
|
562
|
+
// This is a future enhancement - for now, just return current counts
|
|
563
|
+
// Full implementation would query CDP for all active breakpoints
|
|
564
|
+
// and reconcile with state.breakpoints Map
|
|
565
|
+
return {
|
|
566
|
+
synced: this.state.breakpoints.size,
|
|
567
|
+
removed: 0,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Diagnose why a breakpoint failed to set (empty locations array)
|
|
572
|
+
* Performs lazy validation to determine exact cause
|
|
573
|
+
*/
|
|
574
|
+
async diagnoseBreakpointFailure(url, lineNumber) {
|
|
575
|
+
// Check if we have this script loaded (with fallback to base URL matching)
|
|
576
|
+
const match = this.findScriptIds(url);
|
|
577
|
+
if (!match) {
|
|
578
|
+
return {
|
|
579
|
+
cause: 'script_not_found',
|
|
580
|
+
message: `Script not loaded: ${url}`,
|
|
581
|
+
scriptUrl: url,
|
|
582
|
+
requestedLine: lineNumber,
|
|
583
|
+
suggestion: 'The script has not been loaded by Chrome yet. Use reloadPage() or navigateTo() to ensure the script loads.'
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
const { scriptIds } = match;
|
|
587
|
+
// Script exists - check each scriptId to find which contains the requested line
|
|
588
|
+
try {
|
|
589
|
+
const { Debugger } = this.client;
|
|
590
|
+
// Try each script to find one that contains the requested line
|
|
591
|
+
for (const scriptId of scriptIds) {
|
|
592
|
+
const source = await Debugger.getScriptSource({ scriptId });
|
|
593
|
+
const totalLines = source.scriptSource.split('\n').length;
|
|
594
|
+
// Check if this script contains the requested line
|
|
595
|
+
if (lineNumber <= totalLines) {
|
|
596
|
+
// This script contains the line - check if it's executable
|
|
597
|
+
return {
|
|
598
|
+
cause: 'line_not_executable',
|
|
599
|
+
message: `Line ${lineNumber} is not executable code`,
|
|
600
|
+
scriptUrl: url,
|
|
601
|
+
requestedLine: lineNumber,
|
|
602
|
+
totalLines: totalLines,
|
|
603
|
+
suggestion: 'This line may be a comment, blank line, or non-executable declaration. Try setting the breakpoint on a nearby line with executable code (function call, assignment, etc.).'
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// Line number exceeds all scripts - get the maximum lines from all scripts
|
|
608
|
+
let maxLines = 0;
|
|
609
|
+
for (const scriptId of scriptIds) {
|
|
610
|
+
const source = await Debugger.getScriptSource({ scriptId });
|
|
611
|
+
const lineCount = source.scriptSource.split('\n').length;
|
|
612
|
+
maxLines = Math.max(maxLines, lineCount);
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
cause: 'line_out_of_bounds',
|
|
616
|
+
message: `Line ${lineNumber} is out of bounds`,
|
|
617
|
+
scriptUrl: url,
|
|
618
|
+
requestedLine: lineNumber,
|
|
619
|
+
totalLines: maxLines,
|
|
620
|
+
suggestion: scriptIds.length > 1
|
|
621
|
+
? `This URL has ${scriptIds.length} inline scripts. The largest has ${maxLines} lines. Use searchCode() to find the correct script and line.`
|
|
622
|
+
: `The script only has ${maxLines} lines. Use getSourceCode() to view the file and find valid line numbers.`
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
// Fallback if we can't get script source
|
|
627
|
+
return {
|
|
628
|
+
cause: 'script_not_found',
|
|
629
|
+
message: `Unable to access script: ${url}`,
|
|
630
|
+
scriptUrl: url,
|
|
631
|
+
requestedLine: lineNumber,
|
|
632
|
+
suggestion: 'The script may have been unloaded. Try reloadPage().'
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Resume execution
|
|
638
|
+
*/
|
|
639
|
+
async resume() {
|
|
640
|
+
if (!this.state.connected) {
|
|
641
|
+
throw new Error('Not connected to debugger');
|
|
642
|
+
}
|
|
643
|
+
const { Debugger } = this.client;
|
|
644
|
+
await Debugger.resume();
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Pause execution
|
|
648
|
+
*/
|
|
649
|
+
async pause() {
|
|
650
|
+
if (!this.state.connected) {
|
|
651
|
+
throw new Error('Not connected to debugger');
|
|
652
|
+
}
|
|
653
|
+
const { Debugger } = this.client;
|
|
654
|
+
await Debugger.pause();
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Handle logpoint execution limit exceeded
|
|
658
|
+
* This should be called by the LogpointExecutionTracker when a logpoint hits its limit
|
|
659
|
+
*/
|
|
660
|
+
async handleLogpointLimitExceeded(metadata) {
|
|
661
|
+
// Store the metadata
|
|
662
|
+
this.logpointLimitExceeded = metadata;
|
|
663
|
+
// Pause execution
|
|
664
|
+
await this.pause();
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Get information about the logpoint that exceeded its limit (if any)
|
|
668
|
+
*/
|
|
669
|
+
getLogpointLimitExceeded() {
|
|
670
|
+
return this.logpointLimitExceeded;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Clear the logpoint limit exceeded state
|
|
674
|
+
*/
|
|
675
|
+
clearLogpointLimitExceeded() {
|
|
676
|
+
this.logpointLimitExceeded = null;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Step over (next line)
|
|
680
|
+
*/
|
|
681
|
+
async stepOver() {
|
|
682
|
+
if (!this.state.connected) {
|
|
683
|
+
throw new Error('Not connected to debugger');
|
|
684
|
+
}
|
|
685
|
+
const { Debugger } = this.client;
|
|
686
|
+
await Debugger.stepOver();
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Step into function
|
|
690
|
+
*/
|
|
691
|
+
async stepInto() {
|
|
692
|
+
if (!this.state.connected) {
|
|
693
|
+
throw new Error('Not connected to debugger');
|
|
694
|
+
}
|
|
695
|
+
const { Debugger } = this.client;
|
|
696
|
+
await Debugger.stepInto();
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Step out of function
|
|
700
|
+
*/
|
|
701
|
+
async stepOut() {
|
|
702
|
+
if (!this.state.connected) {
|
|
703
|
+
throw new Error('Not connected to debugger');
|
|
704
|
+
}
|
|
705
|
+
const { Debugger } = this.client;
|
|
706
|
+
await Debugger.stepOut();
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Get current call stack
|
|
710
|
+
*/
|
|
711
|
+
getCallStack() {
|
|
712
|
+
if (!this.state.paused || !this.state.currentCallFrames) {
|
|
713
|
+
return undefined;
|
|
714
|
+
}
|
|
715
|
+
return this.state.currentCallFrames.map((frame) => ({
|
|
716
|
+
callFrameId: frame.callFrameId,
|
|
717
|
+
functionName: frame.functionName || '(anonymous)',
|
|
718
|
+
location: frame.location,
|
|
719
|
+
url: this.scriptIdToUrl.get(frame.location.scriptId) || 'unknown',
|
|
720
|
+
scopeChain: frame.scopeChain,
|
|
721
|
+
}));
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Get variables for a specific call frame
|
|
725
|
+
*/
|
|
726
|
+
/**
|
|
727
|
+
* Estimate token count for a value (rough approximation: ~4 chars per token)
|
|
728
|
+
*/
|
|
729
|
+
estimateTokens(value) {
|
|
730
|
+
const str = typeof value === 'string' ? value : JSON.stringify(value);
|
|
731
|
+
return Math.ceil(str.length / 4);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Calculate effective token budget accounting for response overhead.
|
|
735
|
+
* The final MCP response includes:
|
|
736
|
+
* - Message template text (~100 tokens)
|
|
737
|
+
* - JSON code block markers and wrapper (~50 tokens)
|
|
738
|
+
* - JSON.stringify indentation adds ~30% to data size
|
|
739
|
+
*/
|
|
740
|
+
calculateEffectiveBudget(maxTokens) {
|
|
741
|
+
const FIXED_OVERHEAD = 200; // Message template + code block + wrapper
|
|
742
|
+
const PROPORTIONAL_OVERHEAD = 1.3; // JSON indentation adds ~30%
|
|
743
|
+
return Math.floor((maxTokens - FIXED_OVERHEAD) / PROPORTIONAL_OVERHEAD);
|
|
744
|
+
}
|
|
745
|
+
async getVariables(callFrameId, includeGlobal = false, filter, expandObjects = true, maxDepth = 2, maxTokens = 1000) {
|
|
746
|
+
if (!this.state.connected) {
|
|
747
|
+
throw new Error('Not connected to debugger');
|
|
748
|
+
}
|
|
749
|
+
const { Runtime } = this.client;
|
|
750
|
+
const callFrame = this.state.currentCallFrames?.find((frame) => frame.callFrameId === callFrameId);
|
|
751
|
+
if (!callFrame) {
|
|
752
|
+
throw new Error(`Call frame ${callFrameId} not found`);
|
|
753
|
+
}
|
|
754
|
+
let filterRegex = null;
|
|
755
|
+
if (filter) {
|
|
756
|
+
try {
|
|
757
|
+
filterRegex = new RegExp(filter, 'i');
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
throw new Error(`Invalid filter regex: ${filter}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const requestedDepth = maxDepth;
|
|
764
|
+
// Collect property metadata first (without expanding)
|
|
765
|
+
const propertyMeta = [];
|
|
766
|
+
for (const scope of callFrame.scopeChain) {
|
|
767
|
+
if (scope.type === 'global' && !includeGlobal) {
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const properties = await Runtime.getProperties({
|
|
771
|
+
objectId: scope.object.objectId,
|
|
772
|
+
ownProperties: true,
|
|
773
|
+
});
|
|
774
|
+
for (const prop of properties.result) {
|
|
775
|
+
if (!prop.value)
|
|
776
|
+
continue;
|
|
777
|
+
if (filterRegex && !filterRegex.test(prop.name))
|
|
778
|
+
continue;
|
|
779
|
+
propertyMeta.push({ prop, scopeType: scope.type });
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
const totalCount = propertyMeta.length;
|
|
783
|
+
const effectiveBudget = this.calculateEffectiveBudget(maxTokens);
|
|
784
|
+
const hasFilter = !!filter;
|
|
785
|
+
// Helper to group variables by scope
|
|
786
|
+
const groupByScope = (variables) => {
|
|
787
|
+
const grouped = {};
|
|
788
|
+
for (const v of variables) {
|
|
789
|
+
const scopeType = v.scopeType || 'unknown';
|
|
790
|
+
if (!grouped[scopeType])
|
|
791
|
+
grouped[scopeType] = [];
|
|
792
|
+
grouped[scopeType].push({ name: v.name, value: v.value, type: v.type });
|
|
793
|
+
}
|
|
794
|
+
return grouped;
|
|
795
|
+
};
|
|
796
|
+
// Helper to compute variables at a given depth
|
|
797
|
+
const computeAtDepth = async (depth) => {
|
|
798
|
+
const variables = [];
|
|
799
|
+
const shouldExpand = expandObjects && depth > 0;
|
|
800
|
+
for (const { prop, scopeType } of propertyMeta) {
|
|
801
|
+
variables.push({
|
|
802
|
+
name: prop.name,
|
|
803
|
+
value: await this.formatValue(prop.value, shouldExpand, depth),
|
|
804
|
+
type: prop.value.type,
|
|
805
|
+
scopeType,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
return { variables, tokens: this.estimateTokens(variables) };
|
|
809
|
+
};
|
|
810
|
+
// Helper to get names-only data
|
|
811
|
+
const getNamesOnly = (variables) => {
|
|
812
|
+
const namesByScope = {};
|
|
813
|
+
for (const v of variables) {
|
|
814
|
+
const scopeType = v.scopeType || 'unknown';
|
|
815
|
+
if (!namesByScope[scopeType])
|
|
816
|
+
namesByScope[scopeType] = [];
|
|
817
|
+
namesByScope[scopeType].push(v.name);
|
|
818
|
+
}
|
|
819
|
+
return namesByScope;
|
|
820
|
+
};
|
|
821
|
+
// Helper to get counts-only data
|
|
822
|
+
const getCountsOnly = (variables) => {
|
|
823
|
+
const countsByScope = {};
|
|
824
|
+
for (const v of variables) {
|
|
825
|
+
const scopeType = v.scopeType || 'unknown';
|
|
826
|
+
countsByScope[scopeType] = (countsByScope[scopeType] || 0) + 1;
|
|
827
|
+
}
|
|
828
|
+
return countsByScope;
|
|
829
|
+
};
|
|
830
|
+
// Step 1: Try full data at requested depth
|
|
831
|
+
if (expandObjects && maxDepth > 0) {
|
|
832
|
+
const requestedResult = await computeAtDepth(maxDepth);
|
|
833
|
+
if (requestedResult.tokens <= effectiveBudget) {
|
|
834
|
+
return {
|
|
835
|
+
data: groupByScope(requestedResult.variables),
|
|
836
|
+
totalCount,
|
|
837
|
+
usedDepth: maxDepth,
|
|
838
|
+
requestedDepth,
|
|
839
|
+
responseType: 'full',
|
|
840
|
+
filterInsufficient: false,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
// Step 2: Try full data at depth 0
|
|
845
|
+
const depth0Result = await computeAtDepth(0);
|
|
846
|
+
if (depth0Result.tokens <= effectiveBudget) {
|
|
847
|
+
// If we got here after trying requested depth, it means depth was reduced
|
|
848
|
+
const wasDepthReduced = expandObjects && maxDepth > 0;
|
|
849
|
+
return {
|
|
850
|
+
data: groupByScope(depth0Result.variables),
|
|
851
|
+
totalCount,
|
|
852
|
+
usedDepth: 0,
|
|
853
|
+
requestedDepth,
|
|
854
|
+
responseType: wasDepthReduced ? 'depth_reduced' : 'full',
|
|
855
|
+
filterInsufficient: false,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
// Step 3: Try names-only
|
|
859
|
+
const namesOnly = getNamesOnly(depth0Result.variables);
|
|
860
|
+
const namesTokens = this.estimateTokens(namesOnly);
|
|
861
|
+
if (namesTokens <= effectiveBudget) {
|
|
862
|
+
return {
|
|
863
|
+
data: namesOnly,
|
|
864
|
+
totalCount,
|
|
865
|
+
usedDepth: 0,
|
|
866
|
+
requestedDepth,
|
|
867
|
+
responseType: 'names_only',
|
|
868
|
+
filterInsufficient: hasFilter, // Filter was provided but still too large
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
// Step 4: Fall back to counts-only (always fits)
|
|
872
|
+
const countsOnly = getCountsOnly(depth0Result.variables);
|
|
873
|
+
return {
|
|
874
|
+
data: countsOnly,
|
|
875
|
+
totalCount,
|
|
876
|
+
usedDepth: 0,
|
|
877
|
+
requestedDepth,
|
|
878
|
+
responseType: 'counts_only',
|
|
879
|
+
filterInsufficient: hasFilter,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Bounded upper limit on how long evaluateExpression() will wait for CDP to
|
|
884
|
+
* respond, in milliseconds. Exported as an instance property (rather than a
|
|
885
|
+
* module constant) so tests can override it on a per-manager basis without
|
|
886
|
+
* needing real wall-clock waits. Kept comfortably below the 180s default
|
|
887
|
+
* Puppeteer protocolTimeout that would otherwise leak through as a raw,
|
|
888
|
+
* untyped "Runtime.callFunctionOn timed out" error on this path.
|
|
889
|
+
*/
|
|
890
|
+
evaluateExpressionTimeoutMs = 10_000;
|
|
891
|
+
/**
|
|
892
|
+
* Evaluate an expression in the current context.
|
|
893
|
+
*
|
|
894
|
+
* Two distinct failure modes are handled explicitly (this method used to
|
|
895
|
+
* handle neither, which is how a stack-exhaustion RangeError thrown by
|
|
896
|
+
* evaluated code could hang a caller forever with no result, no error, and
|
|
897
|
+
* no timeout):
|
|
898
|
+
*
|
|
899
|
+
* 1. The evaluated code throws. CDP reports this via `exceptionDetails` on
|
|
900
|
+
* an otherwise-successful response - it is NOT a rejected promise - so
|
|
901
|
+
* it must be checked explicitly and surfaced as
|
|
902
|
+
* EvaluateExpressionExceptionError instead of being formatted as if it
|
|
903
|
+
* were a normal return value.
|
|
904
|
+
* 2. The CDP call itself never resolves (a wedged renderer/execution
|
|
905
|
+
* context). A bounded client-side timeout (racing the CDP call against
|
|
906
|
+
* a timer) guarantees this method always settles, throwing
|
|
907
|
+
* EvaluateExpressionTimeoutError instead of hanging indefinitely. We
|
|
908
|
+
* also pass CDP's own `timeout` param so well-behaved renderers abort
|
|
909
|
+
* the runaway script themselves and return exceptionDetails cleanly
|
|
910
|
+
* (case 1) rather than relying solely on our client-side timer.
|
|
911
|
+
*/
|
|
912
|
+
async evaluateExpression(expression, callFrameId, expandObjects = true, maxDepth = 2, options) {
|
|
913
|
+
const detailed = await this.evaluateExpressionDetailed(expression, callFrameId, expandObjects, maxDepth, { ...options, captureRaw: false });
|
|
914
|
+
return detailed.formatted;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Like evaluateExpression(), but also attempts an exact by-value capture of
|
|
918
|
+
* the result when options.captureRaw is set (used for `saveAs` and the
|
|
919
|
+
* structured `_meta.inspect.value`, so captured data does not round-trip
|
|
920
|
+
* through display formatting).
|
|
921
|
+
*
|
|
922
|
+
* Promise handling (bug-015), all empirically verified against Chrome 150:
|
|
923
|
+
* - Running page, no callFrameId: `awaitPromise: true` is passed to
|
|
924
|
+
* Runtime.evaluate, so async results resolve to their settled value and
|
|
925
|
+
* rejections surface as exceptionDetails (-> ExceptionError). A promise
|
|
926
|
+
* that never settles is bounded by the client-side timeout.
|
|
927
|
+
* - Paused (callFrameId given, OR a global evaluation while the debugger is
|
|
928
|
+
* paused - Runtime.evaluate with awaitPromise HANGS in that state, and
|
|
929
|
+
* Debugger.evaluateOnCallFrame silently ignores awaitPromise): we never
|
|
930
|
+
* pass awaitPromise. Instead, a Promise result is resolved through its
|
|
931
|
+
* [[PromiseState]]/[[PromiseResult]] internal properties, which works
|
|
932
|
+
* without running the event loop: already-settled promises yield their
|
|
933
|
+
* exact value (or ExceptionError if rejected); a pending promise fails
|
|
934
|
+
* fast with EvaluateExpressionPendingPromiseError instead of burning the
|
|
935
|
+
* timeout, because it can never settle while the event loop is stopped.
|
|
936
|
+
*/
|
|
937
|
+
async evaluateExpressionDetailed(expression, callFrameId, expandObjects = true, maxDepth = 2, options) {
|
|
938
|
+
if (!this.state.connected) {
|
|
939
|
+
throw new Error('Not connected to debugger');
|
|
940
|
+
}
|
|
941
|
+
const awaitPromise = options?.awaitPromise !== false;
|
|
942
|
+
const captureRaw = options?.captureRaw === true;
|
|
943
|
+
const timeoutMs = this.evaluateExpressionTimeoutMs;
|
|
944
|
+
// Leave CDP's own timeout slightly shorter than ours so a well-behaved
|
|
945
|
+
// renderer's own abort (which yields a clean exceptionDetails response)
|
|
946
|
+
// has a chance to win the race against our client-side backstop.
|
|
947
|
+
const cdpTimeoutMs = Math.max(1000, timeoutMs - 1000);
|
|
948
|
+
let timer;
|
|
949
|
+
const timeoutGuard = new Promise((_, reject) => {
|
|
950
|
+
timer = setTimeout(() => reject(new EvaluateExpressionTimeoutError(truncateExpression(expression), timeoutMs)), timeoutMs);
|
|
951
|
+
});
|
|
952
|
+
// The whole pipeline (evaluate + promise recovery + formatting + raw
|
|
953
|
+
// capture) runs inside the client-side timeout race: the follow-up CDP
|
|
954
|
+
// calls talk to the same possibly-wedged target as the evaluation itself.
|
|
955
|
+
const work = (async () => {
|
|
956
|
+
let result;
|
|
957
|
+
if (callFrameId) {
|
|
958
|
+
const { Debugger } = this.client;
|
|
959
|
+
// awaitPromise is deliberately NOT passed: V8 ignores it on
|
|
960
|
+
// evaluateOnCallFrame (verified), so it cannot be relied on.
|
|
961
|
+
result = await Debugger.evaluateOnCallFrame({ callFrameId, expression, timeout: cdpTimeoutMs });
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
const { Runtime } = this.client;
|
|
965
|
+
// awaitPromise while the debugger is paused hangs the CDP call
|
|
966
|
+
// (event loop stopped -> promise can never settle), so only pass it
|
|
967
|
+
// when the page is actually running.
|
|
968
|
+
const canAwaitInProtocol = awaitPromise && !this.state.paused;
|
|
969
|
+
result = await Runtime.evaluate({
|
|
970
|
+
expression,
|
|
971
|
+
timeout: cdpTimeoutMs,
|
|
972
|
+
...(canAwaitInProtocol ? { awaitPromise: true } : {}),
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
if (result.exceptionDetails) {
|
|
976
|
+
throw this.toEvaluateExpressionExceptionError(expression, result.exceptionDetails);
|
|
977
|
+
}
|
|
978
|
+
let remote = result.result;
|
|
979
|
+
// Paused-context promise recovery (see doc comment above).
|
|
980
|
+
if (awaitPromise && remote?.subtype === 'promise' && remote.objectId) {
|
|
981
|
+
remote = await this.resolveSettledPromiseWhilePaused(expression, remote);
|
|
982
|
+
}
|
|
983
|
+
const formatted = await this.formatValue(remote, expandObjects, maxDepth);
|
|
984
|
+
if (!captureRaw) {
|
|
985
|
+
return { formatted, rawCaptured: false };
|
|
986
|
+
}
|
|
987
|
+
const capture = await this.captureRawValue(remote);
|
|
988
|
+
return { formatted, ...capture };
|
|
989
|
+
})();
|
|
990
|
+
try {
|
|
991
|
+
return await Promise.race([work, timeoutGuard]);
|
|
992
|
+
}
|
|
993
|
+
finally {
|
|
994
|
+
clearTimeout(timer);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
/** Map CDP exceptionDetails to a typed error (thrown code or awaited promise rejection). */
|
|
998
|
+
toEvaluateExpressionExceptionError(expression, details) {
|
|
999
|
+
const exceptionObject = details.exception;
|
|
1000
|
+
const exceptionType = exceptionObject?.className || exceptionObject?.subtype || 'Error';
|
|
1001
|
+
// `description` on an Error RemoteObject is "Message\n at stack...".
|
|
1002
|
+
// A promise rejected with a primitive has no description - the reason is
|
|
1003
|
+
// carried as exception.value (details.text is just "Uncaught (in promise)").
|
|
1004
|
+
const description = exceptionObject?.description;
|
|
1005
|
+
const exceptionMessage = description?.split('\n')[0] ||
|
|
1006
|
+
(exceptionObject && exceptionObject.value !== undefined ? String(exceptionObject.value) : undefined) ||
|
|
1007
|
+
details.text ||
|
|
1008
|
+
'Unknown error';
|
|
1009
|
+
const exceptionStack = description?.includes('\n') ? description : undefined;
|
|
1010
|
+
return new EvaluateExpressionExceptionError(truncateExpression(expression), exceptionType, exceptionMessage, exceptionStack);
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Resolve a Promise RemoteObject without running the event loop, via its
|
|
1014
|
+
* [[PromiseState]]/[[PromiseResult]] internal properties (readable even
|
|
1015
|
+
* while the debugger is paused). Returns the settled value's RemoteObject;
|
|
1016
|
+
* throws for rejected/pending promises.
|
|
1017
|
+
*/
|
|
1018
|
+
async resolveSettledPromiseWhilePaused(expression, promiseRemote) {
|
|
1019
|
+
const { Runtime } = this.client;
|
|
1020
|
+
const props = await Runtime.getProperties({ objectId: promiseRemote.objectId, ownProperties: true });
|
|
1021
|
+
const internal = props.internalProperties || [];
|
|
1022
|
+
const state = internal.find((p) => p.name === '[[PromiseState]]')?.value?.value;
|
|
1023
|
+
const resultRemote = internal.find((p) => p.name === '[[PromiseResult]]')?.value;
|
|
1024
|
+
if (state === 'fulfilled') {
|
|
1025
|
+
return resultRemote ?? { type: 'undefined' };
|
|
1026
|
+
}
|
|
1027
|
+
if (state === 'rejected') {
|
|
1028
|
+
const type = resultRemote?.className || resultRemote?.subtype || 'Error';
|
|
1029
|
+
const description = resultRemote?.description;
|
|
1030
|
+
const message = description?.split('\n')[0] ||
|
|
1031
|
+
(resultRemote && resultRemote.value !== undefined ? String(resultRemote.value) : 'Promise rejected');
|
|
1032
|
+
throw new EvaluateExpressionExceptionError(truncateExpression(expression), type, message, description?.includes('\n') ? description : undefined);
|
|
1033
|
+
}
|
|
1034
|
+
// pending (or internal props unavailable): it cannot settle while paused.
|
|
1035
|
+
throw new EvaluateExpressionPendingPromiseError(truncateExpression(expression));
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Attempt an exact by-value capture of an evaluation result, WITHOUT
|
|
1039
|
+
* re-executing the expression: primitives come straight off the
|
|
1040
|
+
* RemoteObject; plain objects/arrays are serialized in place via
|
|
1041
|
+
* Runtime.callFunctionOn(returnByValue). Values whose by-value form is
|
|
1042
|
+
* lossy or impossible (DOM nodes, Date, Map/Set, functions, window - CDP
|
|
1043
|
+
* serializes these to {} or errors) report rawCaptured: false so callers
|
|
1044
|
+
* can fall back to the display-derived reconstruction.
|
|
1045
|
+
*/
|
|
1046
|
+
async captureRawValue(remote) {
|
|
1047
|
+
if (!remote || remote.type === 'undefined') {
|
|
1048
|
+
return { rawValue: undefined, rawCaptured: true };
|
|
1049
|
+
}
|
|
1050
|
+
if (remote.type === 'string' || remote.type === 'number' || remote.type === 'boolean') {
|
|
1051
|
+
if ('value' in remote) {
|
|
1052
|
+
return { rawValue: remote.value, rawCaptured: true };
|
|
1053
|
+
}
|
|
1054
|
+
// Unserializable numbers (NaN, Infinity, -0) arrive as
|
|
1055
|
+
// unserializableValue with no `value` - let the caller fall back.
|
|
1056
|
+
return { rawCaptured: false };
|
|
1057
|
+
}
|
|
1058
|
+
if (remote.type === 'object') {
|
|
1059
|
+
if (remote.subtype === 'null') {
|
|
1060
|
+
return { rawValue: null, rawCaptured: true };
|
|
1061
|
+
}
|
|
1062
|
+
// Only plain JSON-like shapes serialize faithfully by value. Anything
|
|
1063
|
+
// with a specialized subtype/class (date, map, set, node, typedarray,
|
|
1064
|
+
// error, ...) collapses to {} under CDP's by-value serialization, which
|
|
1065
|
+
// is WORSE than the display-derived fallback.
|
|
1066
|
+
const className = remote.className;
|
|
1067
|
+
const jsonLike = (remote.subtype === undefined || remote.subtype === 'array') &&
|
|
1068
|
+
(className === undefined || className === 'Object' || className === 'Array');
|
|
1069
|
+
if (jsonLike && remote.objectId) {
|
|
1070
|
+
try {
|
|
1071
|
+
const { Runtime } = this.client;
|
|
1072
|
+
const cf = await Runtime.callFunctionOn({
|
|
1073
|
+
objectId: remote.objectId,
|
|
1074
|
+
functionDeclaration: 'function() { return this; }',
|
|
1075
|
+
returnByValue: true,
|
|
1076
|
+
});
|
|
1077
|
+
if (!cf.exceptionDetails) {
|
|
1078
|
+
return { rawValue: cf.result?.value, rawCaptured: true };
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
// Non-serializable (e.g. window: "Object reference chain is too
|
|
1083
|
+
// long") - fall through to the display-derived fallback.
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return { rawCaptured: false };
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Get available variables at a specific source location
|
|
1091
|
+
* Useful for validating logpoint expressions
|
|
1092
|
+
*/
|
|
1093
|
+
async getScopeVariablesAtLocation(url, lineNumber) {
|
|
1094
|
+
if (!this.state.connected) {
|
|
1095
|
+
throw new Error('Not connected to debugger');
|
|
1096
|
+
}
|
|
1097
|
+
// Temporarily set a breakpoint to inspect scope
|
|
1098
|
+
const tempBreakpoint = await this.setBreakpoint(url, lineNumber);
|
|
1099
|
+
try {
|
|
1100
|
+
// Wait for the breakpoint to be hit (with timeout)
|
|
1101
|
+
// Note: This requires the code to actually execute
|
|
1102
|
+
// For static analysis, we'd need a different approach
|
|
1103
|
+
// For now, return null to indicate we can't determine scope without execution
|
|
1104
|
+
// This would require the debugger to be paused at that location
|
|
1105
|
+
if (!this.state.paused || !this.state.currentCallFrames) {
|
|
1106
|
+
// Remove the temporary breakpoint
|
|
1107
|
+
await this.removeBreakpoint(tempBreakpoint.breakpointId);
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
// Get the call frame
|
|
1111
|
+
const callFrame = this.state.currentCallFrames[0];
|
|
1112
|
+
// Extract variable names from all scopes
|
|
1113
|
+
const { Runtime } = this.client;
|
|
1114
|
+
const scopes = [];
|
|
1115
|
+
const allVariables = [];
|
|
1116
|
+
for (const scope of callFrame.scopeChain) {
|
|
1117
|
+
if (scope.type === 'global')
|
|
1118
|
+
continue; // Skip global scope
|
|
1119
|
+
const properties = await Runtime.getProperties({
|
|
1120
|
+
objectId: scope.object.objectId,
|
|
1121
|
+
ownProperties: true,
|
|
1122
|
+
});
|
|
1123
|
+
const variableNames = properties.result
|
|
1124
|
+
.filter((prop) => prop.value && !prop.name.startsWith('[['))
|
|
1125
|
+
.map((prop) => prop.name);
|
|
1126
|
+
scopes.push({
|
|
1127
|
+
type: scope.type,
|
|
1128
|
+
variables: variableNames,
|
|
1129
|
+
});
|
|
1130
|
+
allVariables.push(...variableNames);
|
|
1131
|
+
}
|
|
1132
|
+
// Remove the temporary breakpoint
|
|
1133
|
+
await this.removeBreakpoint(tempBreakpoint.breakpointId);
|
|
1134
|
+
return {
|
|
1135
|
+
variables: [...new Set(allVariables)], // Deduplicate
|
|
1136
|
+
scopes,
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
catch (error) {
|
|
1140
|
+
// Clean up the breakpoint if something goes wrong
|
|
1141
|
+
try {
|
|
1142
|
+
await this.removeBreakpoint(tempBreakpoint.breakpointId);
|
|
1143
|
+
}
|
|
1144
|
+
catch (e) {
|
|
1145
|
+
// Ignore cleanup errors
|
|
1146
|
+
}
|
|
1147
|
+
throw error;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Validate logpoint expressions at a specific location
|
|
1152
|
+
* Sets temp breakpoint, waits for execution, tests expressions
|
|
1153
|
+
*
|
|
1154
|
+
* @param url File URL (e.g., http://localhost:3000/app.js)
|
|
1155
|
+
* @param lineNumber Line number (1-based, will be converted to 0-based for CDP)
|
|
1156
|
+
* @param columnNumber Optional column number (1-based, will be converted to 0-based for CDP)
|
|
1157
|
+
* @param expressions Array of expressions to validate (e.g., ["user.name", "user.id"])
|
|
1158
|
+
* @param timeout Max wait time for execution in milliseconds
|
|
1159
|
+
* @returns Validation results with pass/fail for each expression, plus available variables
|
|
1160
|
+
*/
|
|
1161
|
+
async validateLogpointAtActualLocation(url, lineNumber, columnNumber, expressions, timeout = 2000) {
|
|
1162
|
+
if (!this.state.connected) {
|
|
1163
|
+
throw new Error('Not connected to debugger');
|
|
1164
|
+
}
|
|
1165
|
+
// Note: lineNumber and columnNumber are 1-based (user input)
|
|
1166
|
+
// setBreakpoint will convert them to 0-based for CDP
|
|
1167
|
+
const tempBreakpoint = await this.setBreakpoint(url, lineNumber, columnNumber);
|
|
1168
|
+
try {
|
|
1169
|
+
// Wait for execution with timeout
|
|
1170
|
+
await new Promise(resolve => setTimeout(resolve, timeout));
|
|
1171
|
+
// Check if we paused at the breakpoint
|
|
1172
|
+
if (!this.state.paused || !this.state.currentCallFrames) {
|
|
1173
|
+
// Code didn't execute - remove breakpoint and return
|
|
1174
|
+
await this.removeBreakpoint(tempBreakpoint.breakpointId);
|
|
1175
|
+
return {
|
|
1176
|
+
executed: false,
|
|
1177
|
+
allValid: false,
|
|
1178
|
+
results: expressions.map(expr => ({
|
|
1179
|
+
expression: expr,
|
|
1180
|
+
valid: false,
|
|
1181
|
+
error: 'Code has not executed yet - cannot validate without execution'
|
|
1182
|
+
})),
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
// Get actual location from CDP (0-based)
|
|
1186
|
+
const actualLocation = tempBreakpoint.location;
|
|
1187
|
+
const actualLineUser = actualLocation.lineNumber + 1; // Convert 0-based → 1-based
|
|
1188
|
+
const actualColumnUser = actualLocation.columnNumber !== undefined
|
|
1189
|
+
? actualLocation.columnNumber + 1 // Convert 0-based → 1-based
|
|
1190
|
+
: undefined;
|
|
1191
|
+
// Get call frame for evaluation
|
|
1192
|
+
const callFrame = this.state.currentCallFrames[0];
|
|
1193
|
+
// Collect all available variables
|
|
1194
|
+
const { Runtime } = this.client;
|
|
1195
|
+
const availableVariables = [];
|
|
1196
|
+
for (const scope of callFrame.scopeChain) {
|
|
1197
|
+
if (scope.type === 'global')
|
|
1198
|
+
continue; // Skip global
|
|
1199
|
+
const properties = await Runtime.getProperties({
|
|
1200
|
+
objectId: scope.object.objectId,
|
|
1201
|
+
ownProperties: true,
|
|
1202
|
+
});
|
|
1203
|
+
properties.result
|
|
1204
|
+
.filter((prop) => prop.value && !prop.name.startsWith('[['))
|
|
1205
|
+
.forEach((prop) => availableVariables.push(prop.name));
|
|
1206
|
+
}
|
|
1207
|
+
// Evaluate each expression
|
|
1208
|
+
const results = [];
|
|
1209
|
+
for (const expr of expressions) {
|
|
1210
|
+
try {
|
|
1211
|
+
const value = await this.evaluateExpression(expr, callFrame.callFrameId);
|
|
1212
|
+
results.push({
|
|
1213
|
+
expression: expr,
|
|
1214
|
+
valid: true,
|
|
1215
|
+
value,
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
results.push({
|
|
1220
|
+
expression: expr,
|
|
1221
|
+
valid: false,
|
|
1222
|
+
error: String(error),
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
// Resume execution
|
|
1227
|
+
await this.resume();
|
|
1228
|
+
// Remove temp breakpoint
|
|
1229
|
+
await this.removeBreakpoint(tempBreakpoint.breakpointId);
|
|
1230
|
+
const allValid = results.every(r => r.valid);
|
|
1231
|
+
return {
|
|
1232
|
+
executed: true,
|
|
1233
|
+
allValid,
|
|
1234
|
+
results,
|
|
1235
|
+
availableVariables: [...new Set(availableVariables)], // Deduplicate
|
|
1236
|
+
actualLocation: {
|
|
1237
|
+
line: actualLineUser,
|
|
1238
|
+
column: actualColumnUser,
|
|
1239
|
+
},
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
catch (error) {
|
|
1243
|
+
// Clean up on error
|
|
1244
|
+
try {
|
|
1245
|
+
if (this.state.paused) {
|
|
1246
|
+
await this.resume();
|
|
1247
|
+
}
|
|
1248
|
+
await this.removeBreakpoint(tempBreakpoint.breakpointId);
|
|
1249
|
+
}
|
|
1250
|
+
catch (e) {
|
|
1251
|
+
// Ignore cleanup errors
|
|
1252
|
+
}
|
|
1253
|
+
throw error;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Search for the best location to set a logpoint by trying nearby lines/columns
|
|
1258
|
+
* Returns suggestions ranked by how many expressions are valid
|
|
1259
|
+
*
|
|
1260
|
+
* @param url File URL
|
|
1261
|
+
* @param lineNumber Desired line number (1-based)
|
|
1262
|
+
* @param columnNumber Desired column number (1-based, optional)
|
|
1263
|
+
* @param expressions Array of expressions to validate
|
|
1264
|
+
* @param searchRadius Number of lines to search in each direction (default: 2)
|
|
1265
|
+
* @param timeout Timeout per location test in milliseconds (default: 1000ms)
|
|
1266
|
+
* @returns Array of suggestions sorted by score (best first), max 3 results
|
|
1267
|
+
*/
|
|
1268
|
+
async findBestLogpointLocation(url, lineNumber, columnNumber, expressions, searchRadius = 2, timeout = 1000) {
|
|
1269
|
+
const candidates = [];
|
|
1270
|
+
// Try requested line first, then ±1, ±2, etc.
|
|
1271
|
+
for (let offset = 0; offset <= searchRadius; offset++) {
|
|
1272
|
+
const lines = offset === 0 ? [lineNumber] : [lineNumber - offset, lineNumber + offset];
|
|
1273
|
+
for (const line of lines) {
|
|
1274
|
+
if (line < 1)
|
|
1275
|
+
continue; // Skip negative lines
|
|
1276
|
+
try {
|
|
1277
|
+
const validation = await this.validateLogpointAtActualLocation(url, line, columnNumber, expressions, timeout);
|
|
1278
|
+
if (validation.executed) {
|
|
1279
|
+
const validCount = validation.results.filter(r => r.valid).length;
|
|
1280
|
+
const score = Math.round((validCount / expressions.length) * 100);
|
|
1281
|
+
candidates.push({
|
|
1282
|
+
line: validation.actualLocation?.line || line,
|
|
1283
|
+
column: validation.actualLocation?.column,
|
|
1284
|
+
score,
|
|
1285
|
+
validExpressions: validation.results.filter(r => r.valid).map(r => r.expression),
|
|
1286
|
+
invalidExpressions: validation.results.filter(r => !r.valid).map(r => r.expression),
|
|
1287
|
+
availableVariables: validation.availableVariables || [],
|
|
1288
|
+
reason: score === 100 ? 'All expressions available in scope' :
|
|
1289
|
+
score > 0 ? `${validCount}/${expressions.length} expressions available` :
|
|
1290
|
+
'No expressions available'
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
catch (e) {
|
|
1295
|
+
// Skip locations that error
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
// Sort by score (highest first), then by proximity to original line
|
|
1301
|
+
candidates.sort((a, b) => {
|
|
1302
|
+
if (b.score !== a.score) {
|
|
1303
|
+
return b.score - a.score; // Higher score first
|
|
1304
|
+
}
|
|
1305
|
+
// If scores equal, prefer closer to original line
|
|
1306
|
+
return Math.abs(a.line - lineNumber) - Math.abs(b.line - lineNumber);
|
|
1307
|
+
});
|
|
1308
|
+
// Return top 3
|
|
1309
|
+
return candidates.slice(0, 3);
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Get source code from a file at a specific line range
|
|
1313
|
+
*/
|
|
1314
|
+
async getSourceCode(url, startLine, endLine) {
|
|
1315
|
+
if (!this.state.connected) {
|
|
1316
|
+
throw new Error('Not connected to debugger');
|
|
1317
|
+
}
|
|
1318
|
+
const { Debugger } = this.client;
|
|
1319
|
+
// Find the script IDs for this URL (with fallback matching for partial URLs)
|
|
1320
|
+
const match = this.findScriptIds(url);
|
|
1321
|
+
if (!match) {
|
|
1322
|
+
throw new Error(`Script not found for URL: ${url}. Make sure the script has been loaded/parsed.`);
|
|
1323
|
+
}
|
|
1324
|
+
const scriptIds = match.scriptIds;
|
|
1325
|
+
// If multiple scripts, find the one containing the requested line range
|
|
1326
|
+
let scriptId;
|
|
1327
|
+
let fullSource;
|
|
1328
|
+
let totalLines;
|
|
1329
|
+
if (scriptIds.length === 1) {
|
|
1330
|
+
// Only one script - use it
|
|
1331
|
+
scriptId = scriptIds[0];
|
|
1332
|
+
const result = await Debugger.getScriptSource({ scriptId });
|
|
1333
|
+
fullSource = result.scriptSource;
|
|
1334
|
+
totalLines = fullSource.split('\n').length;
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
// Multiple scripts - find the one containing the requested line
|
|
1338
|
+
const targetLine = startLine || 1;
|
|
1339
|
+
let found = false;
|
|
1340
|
+
for (const sid of scriptIds) {
|
|
1341
|
+
const result = await Debugger.getScriptSource({ scriptId: sid });
|
|
1342
|
+
const source = result.scriptSource;
|
|
1343
|
+
const lineCount = source.split('\n').length;
|
|
1344
|
+
// Check if this script contains the requested line
|
|
1345
|
+
if (targetLine <= lineCount) {
|
|
1346
|
+
scriptId = sid;
|
|
1347
|
+
fullSource = source;
|
|
1348
|
+
totalLines = lineCount;
|
|
1349
|
+
found = true;
|
|
1350
|
+
break;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (!found) {
|
|
1354
|
+
throw new Error(`Line ${targetLine} not found in any script for URL: ${url}. This URL has ${scriptIds.length} inline scripts.`);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
// Split into lines
|
|
1358
|
+
const lines = fullSource.split('\n');
|
|
1359
|
+
// Determine the range
|
|
1360
|
+
const start = startLine ? Math.max(1, startLine) : 1;
|
|
1361
|
+
const end = endLine ? Math.min(totalLines, endLine) : (startLine ? Math.min(totalLines, startLine + 9) : totalLines);
|
|
1362
|
+
// Extract the requested lines (convert to 0-indexed)
|
|
1363
|
+
const extractedLines = lines.slice(start - 1, end);
|
|
1364
|
+
// Format with line numbers, truncating source map lines
|
|
1365
|
+
const formattedCode = extractedLines
|
|
1366
|
+
.map((line, index) => {
|
|
1367
|
+
const lineNum = start + index;
|
|
1368
|
+
let displayLine = line;
|
|
1369
|
+
// Detect and truncate inline source maps (they can be huge)
|
|
1370
|
+
if (line.startsWith('//# sourceMappingURL=data:')) {
|
|
1371
|
+
const truncateAt = 60;
|
|
1372
|
+
displayLine = line.substring(0, truncateAt) + `... [source map truncated, ${Math.round(line.length / 1024)}kb]`;
|
|
1373
|
+
}
|
|
1374
|
+
return `${String(lineNum).padStart(4, ' ')} | ${displayLine}`;
|
|
1375
|
+
})
|
|
1376
|
+
.join('\n');
|
|
1377
|
+
// Check if source map is available
|
|
1378
|
+
const hasSourceMap = this.sourceMapHandler?.hasSourceMap(url) || false;
|
|
1379
|
+
return {
|
|
1380
|
+
code: formattedCode,
|
|
1381
|
+
totalLines: totalLines,
|
|
1382
|
+
hasSourceMap,
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Inject a clickable console link in the browser
|
|
1387
|
+
*/
|
|
1388
|
+
async injectConsoleLink(url, lineNumber, message) {
|
|
1389
|
+
if (!this.state.connected) {
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
const { Runtime } = this.client;
|
|
1393
|
+
const consoleExpression = `
|
|
1394
|
+
console.log(
|
|
1395
|
+
'${message} %c${url}:${lineNumber}%c',
|
|
1396
|
+
'color: #0066cc; text-decoration: underline; cursor: pointer; font-weight: bold',
|
|
1397
|
+
);
|
|
1398
|
+
`;
|
|
1399
|
+
try {
|
|
1400
|
+
await Runtime.evaluate({ expression: consoleExpression });
|
|
1401
|
+
}
|
|
1402
|
+
catch (error) {
|
|
1403
|
+
// Ignore errors if console injection fails
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Get detailed information about current pause state
|
|
1408
|
+
*/
|
|
1409
|
+
getPausedInfo() {
|
|
1410
|
+
if (!this.state.paused) {
|
|
1411
|
+
return { paused: false };
|
|
1412
|
+
}
|
|
1413
|
+
const callStack = this.getCallStack();
|
|
1414
|
+
const location = callStack && callStack.length > 0 ? {
|
|
1415
|
+
url: callStack[0].url,
|
|
1416
|
+
lineNumber: callStack[0].location.lineNumber,
|
|
1417
|
+
columnNumber: callStack[0].location.columnNumber,
|
|
1418
|
+
functionName: callStack[0].functionName,
|
|
1419
|
+
} : undefined;
|
|
1420
|
+
return {
|
|
1421
|
+
paused: true,
|
|
1422
|
+
location,
|
|
1423
|
+
callStack,
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Wait for debugger to pause (for race detection)
|
|
1428
|
+
* Returns a promise that resolves when debugger pauses, or rejects on timeout
|
|
1429
|
+
*/
|
|
1430
|
+
waitForPause(timeoutMs = 30000) {
|
|
1431
|
+
debugLog('cdp-manager', `waitForPause called, already paused: ${this.state.paused}, timeout: ${timeoutMs}ms`);
|
|
1432
|
+
if (this.state.paused) {
|
|
1433
|
+
debugLog('cdp-manager', 'waitForPause: already paused, resolving immediately');
|
|
1434
|
+
return Promise.resolve();
|
|
1435
|
+
}
|
|
1436
|
+
return new Promise((resolve, reject) => {
|
|
1437
|
+
let resolved = false;
|
|
1438
|
+
const wrappedResolve = () => {
|
|
1439
|
+
if (resolved)
|
|
1440
|
+
return;
|
|
1441
|
+
resolved = true;
|
|
1442
|
+
debugLog('cdp-manager', 'waitForPause: wrappedResolve called, resolving promise');
|
|
1443
|
+
clearTimeout(timeout);
|
|
1444
|
+
resolve();
|
|
1445
|
+
};
|
|
1446
|
+
const timeout = setTimeout(() => {
|
|
1447
|
+
if (resolved)
|
|
1448
|
+
return;
|
|
1449
|
+
resolved = true;
|
|
1450
|
+
debugLog('cdp-manager', `waitForPause: timeout after ${timeoutMs}ms, state.paused=${this.state.paused}`);
|
|
1451
|
+
// Remove resolver from list
|
|
1452
|
+
const index = this.pauseResolvers.indexOf(wrappedResolve);
|
|
1453
|
+
if (index > -1) {
|
|
1454
|
+
this.pauseResolvers.splice(index, 1);
|
|
1455
|
+
}
|
|
1456
|
+
reject(new Error('Timeout waiting for pause'));
|
|
1457
|
+
}, timeoutMs);
|
|
1458
|
+
this.pauseResolvers.push(wrappedResolve);
|
|
1459
|
+
debugLog('cdp-manager', `waitForPause: resolver added, total resolvers: ${this.pauseResolvers.length}`);
|
|
1460
|
+
// Re-check after adding resolver to handle race condition
|
|
1461
|
+
// where pause happened between initial check and push
|
|
1462
|
+
if (this.state.paused) {
|
|
1463
|
+
debugLog('cdp-manager', 'waitForPause: paused detected after adding resolver, resolving');
|
|
1464
|
+
wrappedResolve();
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Format a CDP RemoteObject value for display
|
|
1470
|
+
* @param value The CDP RemoteObject to format
|
|
1471
|
+
* @param expandObjects Whether to expand object/array contents (default: false for backward compatibility)
|
|
1472
|
+
* @param maxDepth Maximum depth for object expansion (default: 2)
|
|
1473
|
+
* @param currentDepth Current recursion depth (internal use)
|
|
1474
|
+
*/
|
|
1475
|
+
async formatValue(value, expandObjects = false, maxDepth = 2, currentDepth = 0) {
|
|
1476
|
+
if (value.type === 'undefined')
|
|
1477
|
+
return 'undefined';
|
|
1478
|
+
if (value.type === 'string')
|
|
1479
|
+
return `"${value.value}"`;
|
|
1480
|
+
if (value.type === 'number' || value.type === 'boolean')
|
|
1481
|
+
return String(value.value);
|
|
1482
|
+
if (value.type === 'object') {
|
|
1483
|
+
if (value.subtype === 'null')
|
|
1484
|
+
return 'null';
|
|
1485
|
+
// Detect and handle DOM nodes specially (never expand)
|
|
1486
|
+
const className = value.className || '';
|
|
1487
|
+
if (className.startsWith('HTML') || className.includes('Element') || value.subtype === 'node') {
|
|
1488
|
+
return `[${className || 'DOMNode'}]`;
|
|
1489
|
+
}
|
|
1490
|
+
// If expansion is disabled or we've hit max depth, return description
|
|
1491
|
+
if (!expandObjects || currentDepth >= maxDepth) {
|
|
1492
|
+
if (value.subtype === 'array')
|
|
1493
|
+
return `Array(${value.description})`;
|
|
1494
|
+
return value.description || value.className || 'Object';
|
|
1495
|
+
}
|
|
1496
|
+
// Expand object/array contents with smart size limits
|
|
1497
|
+
if (value.objectId) {
|
|
1498
|
+
try {
|
|
1499
|
+
const { Runtime } = this.client;
|
|
1500
|
+
const properties = await Runtime.getProperties({
|
|
1501
|
+
objectId: value.objectId,
|
|
1502
|
+
ownProperties: true,
|
|
1503
|
+
});
|
|
1504
|
+
if (value.subtype === 'array') {
|
|
1505
|
+
// Smart array handling based on size
|
|
1506
|
+
const numericProps = properties.result.filter((p) => !isNaN(parseInt(p.name, 10)));
|
|
1507
|
+
const arrayLength = numericProps.length;
|
|
1508
|
+
// For large arrays, truncate to first 10 elements
|
|
1509
|
+
if (arrayLength > 20) {
|
|
1510
|
+
const arrayElements = [];
|
|
1511
|
+
let itemsShown = 0;
|
|
1512
|
+
for (const prop of numericProps.slice(0, 10)) {
|
|
1513
|
+
const index = parseInt(prop.name, 10);
|
|
1514
|
+
if (prop.value) {
|
|
1515
|
+
arrayElements[index] = await this.formatValue(prop.value, expandObjects, maxDepth, currentDepth + 1);
|
|
1516
|
+
itemsShown++;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
// Add truncation indicator
|
|
1520
|
+
arrayElements.push(`... ${arrayLength - itemsShown} more items (use evaluateExpression to inspect)`);
|
|
1521
|
+
return arrayElements;
|
|
1522
|
+
}
|
|
1523
|
+
// Small arrays - show all elements
|
|
1524
|
+
const arrayElements = [];
|
|
1525
|
+
for (const prop of properties.result) {
|
|
1526
|
+
const index = parseInt(prop.name, 10);
|
|
1527
|
+
if (!isNaN(index) && prop.value) {
|
|
1528
|
+
arrayElements[index] = await this.formatValue(prop.value, expandObjects, maxDepth, currentDepth + 1);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
return arrayElements;
|
|
1532
|
+
}
|
|
1533
|
+
else {
|
|
1534
|
+
// Smart object handling based on property count
|
|
1535
|
+
const validProps = properties.result.filter((p) => p.value && !p.name.startsWith('[['));
|
|
1536
|
+
const propCount = validProps.length;
|
|
1537
|
+
// For very large objects, show summary with first few keys
|
|
1538
|
+
if (propCount > 50) {
|
|
1539
|
+
const firstKeys = validProps.slice(0, 5).map((p) => p.name);
|
|
1540
|
+
return `[Object with ${propCount} properties] {${firstKeys.join(', ')}, ...} - use evaluateExpression to inspect`;
|
|
1541
|
+
}
|
|
1542
|
+
// For moderately large objects (10-50 props), limit depth
|
|
1543
|
+
if (propCount > 10 && currentDepth > 0) {
|
|
1544
|
+
const firstKeys = validProps.slice(0, 10).map((p) => p.name);
|
|
1545
|
+
return `[Object with ${propCount} properties] {${firstKeys.join(', ')}${propCount > 10 ? ', ...' : ''}}`;
|
|
1546
|
+
}
|
|
1547
|
+
// Small objects - expand normally
|
|
1548
|
+
const objectProps = {};
|
|
1549
|
+
for (const prop of validProps) {
|
|
1550
|
+
objectProps[prop.name] = await this.formatValue(prop.value, expandObjects, maxDepth, currentDepth + 1);
|
|
1551
|
+
}
|
|
1552
|
+
return objectProps;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
catch (error) {
|
|
1556
|
+
// If expansion fails, fall back to description
|
|
1557
|
+
if (value.subtype === 'array')
|
|
1558
|
+
return `Array(${value.description})`;
|
|
1559
|
+
return value.description || value.className || 'Object';
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
// No objectId, can't expand
|
|
1563
|
+
if (value.subtype === 'array')
|
|
1564
|
+
return `Array(${value.description})`;
|
|
1565
|
+
return value.description || value.className || 'Object';
|
|
1566
|
+
}
|
|
1567
|
+
if (value.type === 'function') {
|
|
1568
|
+
// Extract just the function name/signature, not the full source
|
|
1569
|
+
const desc = value.description || '';
|
|
1570
|
+
const match = desc.match(/^(function\s+\w+|async\s+function\s+\w+|\w+)\s*\(/);
|
|
1571
|
+
if (match) {
|
|
1572
|
+
return `[Function: ${match[1]}]`;
|
|
1573
|
+
}
|
|
1574
|
+
return '[Function]';
|
|
1575
|
+
}
|
|
1576
|
+
return String(value.value);
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Expand a CDP RemoteObject by its objectId
|
|
1580
|
+
* This is used to get full object details for console messages
|
|
1581
|
+
* @param objectId - The CDP object ID to expand
|
|
1582
|
+
* @param maxDepth - Maximum depth for nested object expansion (default: 2)
|
|
1583
|
+
*/
|
|
1584
|
+
async expandObjectById(objectId, maxDepth = 2) {
|
|
1585
|
+
if (!this.state.connected || !this.client) {
|
|
1586
|
+
throw new Error('Not connected to debugger');
|
|
1587
|
+
}
|
|
1588
|
+
const { Runtime } = this.client;
|
|
1589
|
+
// Get the object properties
|
|
1590
|
+
const properties = await Runtime.getProperties({
|
|
1591
|
+
objectId,
|
|
1592
|
+
ownProperties: true,
|
|
1593
|
+
});
|
|
1594
|
+
// Build the expanded object
|
|
1595
|
+
const result = {};
|
|
1596
|
+
const validProps = properties.result.filter((p) => p.value && !p.name.startsWith('[['));
|
|
1597
|
+
for (const prop of validProps.slice(0, 20)) { // Limit to 20 properties for console output
|
|
1598
|
+
result[prop.name] = await this.formatValue(prop.value, true, maxDepth, 0);
|
|
1599
|
+
}
|
|
1600
|
+
if (validProps.length > 20) {
|
|
1601
|
+
result['...'] = `(${validProps.length - 20} more properties)`;
|
|
1602
|
+
}
|
|
1603
|
+
return result;
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Get all loaded scripts
|
|
1607
|
+
*/
|
|
1608
|
+
getAllScripts() {
|
|
1609
|
+
return Array.from(this.scriptIdToUrl.entries()).map(([scriptId, url]) => ({
|
|
1610
|
+
scriptId,
|
|
1611
|
+
url,
|
|
1612
|
+
}));
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Search within a specific script using regex
|
|
1616
|
+
*/
|
|
1617
|
+
async searchInScript(scriptId, pattern, caseSensitive = false, isRegex = true) {
|
|
1618
|
+
if (!this.state.connected) {
|
|
1619
|
+
throw new Error('Not connected to debugger');
|
|
1620
|
+
}
|
|
1621
|
+
const { Debugger } = this.client;
|
|
1622
|
+
try {
|
|
1623
|
+
const result = await Debugger.searchInContent({
|
|
1624
|
+
scriptId,
|
|
1625
|
+
query: pattern,
|
|
1626
|
+
caseSensitive,
|
|
1627
|
+
isRegex,
|
|
1628
|
+
});
|
|
1629
|
+
return (result.result || []).map((match) => ({
|
|
1630
|
+
lineNumber: match.lineNumber,
|
|
1631
|
+
lineContent: match.lineContent,
|
|
1632
|
+
}));
|
|
1633
|
+
}
|
|
1634
|
+
catch (error) {
|
|
1635
|
+
// Script might not support search or other error
|
|
1636
|
+
return [];
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Get a specific line from a script by scriptId and line number
|
|
1641
|
+
* Returns the full line content (useful for getting full webpack eval lines)
|
|
1642
|
+
*/
|
|
1643
|
+
async getScriptLine(scriptId, lineNumber) {
|
|
1644
|
+
if (!this.state.connected) {
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
const { Debugger } = this.client;
|
|
1648
|
+
try {
|
|
1649
|
+
const result = await Debugger.getScriptSource({ scriptId });
|
|
1650
|
+
const lines = result.scriptSource.split('\n');
|
|
1651
|
+
// lineNumber is 0-indexed from searchInContent
|
|
1652
|
+
if (lineNumber >= 0 && lineNumber < lines.length) {
|
|
1653
|
+
return lines[lineNumber];
|
|
1654
|
+
}
|
|
1655
|
+
return null;
|
|
1656
|
+
}
|
|
1657
|
+
catch (error) {
|
|
1658
|
+
return null;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
// ============================================
|
|
1662
|
+
// DOMDebugger Methods (DOM/Event/XHR Breakpoints)
|
|
1663
|
+
// ============================================
|
|
1664
|
+
/**
|
|
1665
|
+
* Generate a unique ID for advanced breakpoints
|
|
1666
|
+
*/
|
|
1667
|
+
generateAdvancedBpId(prefix) {
|
|
1668
|
+
return `${prefix}-${++this.advancedBpCounter}`;
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Resolve a CSS selector to a CDP nodeId
|
|
1672
|
+
* @param selector - CSS selector to resolve
|
|
1673
|
+
* @returns nodeId for the element
|
|
1674
|
+
*/
|
|
1675
|
+
async resolveSelector(selector) {
|
|
1676
|
+
if (!this.state.connected) {
|
|
1677
|
+
throw new Error('Not connected to debugger');
|
|
1678
|
+
}
|
|
1679
|
+
const { DOM } = this.client;
|
|
1680
|
+
// Get document root
|
|
1681
|
+
const { root } = await DOM.getDocument();
|
|
1682
|
+
// Query selector to get nodeId
|
|
1683
|
+
const { nodeId } = await DOM.querySelector({
|
|
1684
|
+
nodeId: root.nodeId,
|
|
1685
|
+
selector,
|
|
1686
|
+
});
|
|
1687
|
+
if (!nodeId || nodeId === 0) {
|
|
1688
|
+
throw new Error(`Element not found: ${selector}`);
|
|
1689
|
+
}
|
|
1690
|
+
return nodeId;
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Set a DOM breakpoint on a node
|
|
1694
|
+
* Pauses execution when the specified DOM mutation occurs
|
|
1695
|
+
* @param nodeId - The node ID from resolveSelector
|
|
1696
|
+
* @param type - Type of DOM change to break on
|
|
1697
|
+
* @param selector - Original CSS selector (for reference/display)
|
|
1698
|
+
*/
|
|
1699
|
+
async setDOMBreakpoint(nodeId, type, selector) {
|
|
1700
|
+
if (!this.state.connected) {
|
|
1701
|
+
throw new Error('Not connected to debugger');
|
|
1702
|
+
}
|
|
1703
|
+
const { DOMDebugger } = this.client;
|
|
1704
|
+
await DOMDebugger.setDOMBreakpoint({ nodeId, type });
|
|
1705
|
+
const id = this.generateAdvancedBpId('dom-bp');
|
|
1706
|
+
const info = {
|
|
1707
|
+
breakpointId: id,
|
|
1708
|
+
type: 'dom',
|
|
1709
|
+
nodeId,
|
|
1710
|
+
domBreakpointType: type,
|
|
1711
|
+
selector,
|
|
1712
|
+
};
|
|
1713
|
+
this.domBreakpoints.set(id, info);
|
|
1714
|
+
return info;
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Remove a DOM breakpoint by its ID
|
|
1718
|
+
*/
|
|
1719
|
+
async removeDOMBreakpoint(breakpointId) {
|
|
1720
|
+
const bp = this.domBreakpoints.get(breakpointId);
|
|
1721
|
+
if (!bp) {
|
|
1722
|
+
throw new Error(`DOM breakpoint not found: ${breakpointId}`);
|
|
1723
|
+
}
|
|
1724
|
+
if (!this.state.connected) {
|
|
1725
|
+
throw new Error('Not connected to debugger');
|
|
1726
|
+
}
|
|
1727
|
+
const { DOMDebugger } = this.client;
|
|
1728
|
+
await DOMDebugger.removeDOMBreakpoint({
|
|
1729
|
+
nodeId: bp.nodeId,
|
|
1730
|
+
type: bp.domBreakpointType,
|
|
1731
|
+
});
|
|
1732
|
+
this.domBreakpoints.delete(breakpointId);
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Get all DOM breakpoints
|
|
1736
|
+
*/
|
|
1737
|
+
getDOMBreakpoints() {
|
|
1738
|
+
return Array.from(this.domBreakpoints.values());
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Set an event listener breakpoint
|
|
1742
|
+
* Pauses execution when the specified event fires
|
|
1743
|
+
* @param eventName - DOM event name (e.g., 'click', 'submit', 'input')
|
|
1744
|
+
* @param targetName - Optional EventTarget interface filter (e.g., 'HTMLInputElement')
|
|
1745
|
+
*/
|
|
1746
|
+
async setEventListenerBreakpoint(eventName, targetName) {
|
|
1747
|
+
if (!this.state.connected) {
|
|
1748
|
+
throw new Error('Not connected to debugger');
|
|
1749
|
+
}
|
|
1750
|
+
const { DOMDebugger } = this.client;
|
|
1751
|
+
await DOMDebugger.setEventListenerBreakpoint({ eventName, targetName });
|
|
1752
|
+
const id = this.generateAdvancedBpId('event-bp');
|
|
1753
|
+
const info = {
|
|
1754
|
+
breakpointId: id,
|
|
1755
|
+
type: 'event',
|
|
1756
|
+
eventName,
|
|
1757
|
+
targetName,
|
|
1758
|
+
};
|
|
1759
|
+
this.eventBreakpoints.set(id, info);
|
|
1760
|
+
return info;
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Remove an event listener breakpoint by its ID
|
|
1764
|
+
*/
|
|
1765
|
+
async removeEventListenerBreakpoint(breakpointId) {
|
|
1766
|
+
const bp = this.eventBreakpoints.get(breakpointId);
|
|
1767
|
+
if (!bp) {
|
|
1768
|
+
throw new Error(`Event listener breakpoint not found: ${breakpointId}`);
|
|
1769
|
+
}
|
|
1770
|
+
if (!this.state.connected) {
|
|
1771
|
+
throw new Error('Not connected to debugger');
|
|
1772
|
+
}
|
|
1773
|
+
const { DOMDebugger } = this.client;
|
|
1774
|
+
await DOMDebugger.removeEventListenerBreakpoint({
|
|
1775
|
+
eventName: bp.eventName,
|
|
1776
|
+
targetName: bp.targetName,
|
|
1777
|
+
});
|
|
1778
|
+
this.eventBreakpoints.delete(breakpointId);
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Get all event listener breakpoints
|
|
1782
|
+
*/
|
|
1783
|
+
getEventListenerBreakpoints() {
|
|
1784
|
+
return Array.from(this.eventBreakpoints.values());
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Set an XHR/Fetch breakpoint
|
|
1788
|
+
* Pauses execution when an XHR or Fetch request URL contains the pattern
|
|
1789
|
+
* @param urlPattern - URL substring to match
|
|
1790
|
+
*/
|
|
1791
|
+
async setXHRBreakpoint(urlPattern) {
|
|
1792
|
+
if (!this.state.connected) {
|
|
1793
|
+
throw new Error('Not connected to debugger');
|
|
1794
|
+
}
|
|
1795
|
+
const { DOMDebugger } = this.client;
|
|
1796
|
+
await DOMDebugger.setXHRBreakpoint({ url: urlPattern });
|
|
1797
|
+
const id = this.generateAdvancedBpId('xhr-bp');
|
|
1798
|
+
const info = {
|
|
1799
|
+
breakpointId: id,
|
|
1800
|
+
type: 'xhr',
|
|
1801
|
+
urlPattern,
|
|
1802
|
+
};
|
|
1803
|
+
this.xhrBreakpoints.set(id, info);
|
|
1804
|
+
return info;
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Remove an XHR/Fetch breakpoint by its ID
|
|
1808
|
+
*/
|
|
1809
|
+
async removeXHRBreakpoint(breakpointId) {
|
|
1810
|
+
const bp = this.xhrBreakpoints.get(breakpointId);
|
|
1811
|
+
if (!bp) {
|
|
1812
|
+
throw new Error(`XHR breakpoint not found: ${breakpointId}`);
|
|
1813
|
+
}
|
|
1814
|
+
if (!this.state.connected) {
|
|
1815
|
+
throw new Error('Not connected to debugger');
|
|
1816
|
+
}
|
|
1817
|
+
const { DOMDebugger } = this.client;
|
|
1818
|
+
await DOMDebugger.removeXHRBreakpoint({ url: bp.urlPattern });
|
|
1819
|
+
this.xhrBreakpoints.delete(breakpointId);
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Get all XHR/Fetch breakpoints
|
|
1823
|
+
*/
|
|
1824
|
+
getXHRBreakpoints() {
|
|
1825
|
+
return Array.from(this.xhrBreakpoints.values());
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
//# sourceMappingURL=cdp-manager.js.map
|