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,1397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Automation Tools
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { executeWithPauseDetection } from '../debugger-aware-wrapper.js';
|
|
6
|
+
import { checkBrowserAutomation } from '../error-helpers.js';
|
|
7
|
+
import { createTool } from '../validation-helpers.js';
|
|
8
|
+
import { configManager } from '../config.js';
|
|
9
|
+
import { createSuccessResponse, createErrorResponse } from '../messages.js';
|
|
10
|
+
import { isElementBlocked, detectModals } from '../utils/modal-detector.js';
|
|
11
|
+
import { dismissModalByStrategy, selectDismissalStrategy } from '../utils/modal-dismissal.js';
|
|
12
|
+
import { resolveSelector, isExtendedSelector, cleanupResolvedSelector } from '../utils/selector-resolver.js';
|
|
13
|
+
import { domChangeMonitor, formatDOMChanges } from '../dom-change-monitor.js';
|
|
14
|
+
import { abortErrorFor, abortableSleep, isAbortError, throwIfAborted } from '../utils/abort.js';
|
|
15
|
+
// Coordinate schema for mouse actions
|
|
16
|
+
const coordinateSchema = z.object({
|
|
17
|
+
x: z.number(),
|
|
18
|
+
y: z.number(),
|
|
19
|
+
});
|
|
20
|
+
// Consolidated input tool schema
|
|
21
|
+
const inputToolSchema = z.object({
|
|
22
|
+
action: z.enum(['click', 'type', 'press', 'hover', 'focus', 'focusNext', 'focusPrevious', 'drag', 'scroll', 'mousemove', 'pinch', 'tap', 'swipe']),
|
|
23
|
+
connectionReason: z.string(),
|
|
24
|
+
// Selector-based actions
|
|
25
|
+
selector: z.string().optional().describe('CSS selector. Supports :has-text("x"), :text("x")'),
|
|
26
|
+
handleModals: z.boolean().optional(),
|
|
27
|
+
dismissStrategy: z.enum(['accept', 'reject', 'close', 'remove', 'auto']).optional(),
|
|
28
|
+
// click
|
|
29
|
+
clickCount: z.number().optional(),
|
|
30
|
+
// type
|
|
31
|
+
text: z.string().optional(),
|
|
32
|
+
delay: z.number().optional().describe('Keystroke delay ms'),
|
|
33
|
+
append: z.boolean().optional().describe('Append text instead of replacing (default: false)'),
|
|
34
|
+
// press
|
|
35
|
+
key: z.string().optional(),
|
|
36
|
+
// focusNext/focusPrevious
|
|
37
|
+
count: z.number().optional().describe('Tab count'),
|
|
38
|
+
// drag
|
|
39
|
+
from: coordinateSchema.optional(),
|
|
40
|
+
to: coordinateSchema.optional(),
|
|
41
|
+
steps: z.number().optional().describe('Drag smoothness'),
|
|
42
|
+
// swipe: gesture on an element, instead of from/to pixels
|
|
43
|
+
direction: z.enum(['left', 'right', 'up', 'down']).optional()
|
|
44
|
+
.describe("swipe: gesture direction across `selector`'s box, resolved at run time. Use this rather than from/to for anything in a list - a row's coordinates move with the rows above it"),
|
|
45
|
+
distance: z.number().optional()
|
|
46
|
+
.describe('swipe: travel in px. Default is a short, reveal-sized nudge (60% of the element, capped at 96px) because a long fast swipe commits the destructive action on rows that have ranges - pass a larger distance deliberately to over-drag'),
|
|
47
|
+
durationMs: z.number().optional()
|
|
48
|
+
.describe('swipe: how long the gesture takes, spread across `steps` (default 300 - a deliberate drag). Velocity is a real input to gesture handling: unpaced moves go out as fast as the transport allows, which reads as a flick, and a flick is a DIFFERENT gesture - on a swipe-action row it commits the destructive action instead of revealing it. Pass a small value deliberately to test the flick'),
|
|
49
|
+
// scroll
|
|
50
|
+
deltaX: z.number().optional().describe('Horizontal scroll px'),
|
|
51
|
+
deltaY: z.number().optional().describe('Vertical scroll px'),
|
|
52
|
+
x: z.number().optional(),
|
|
53
|
+
y: z.number().optional(),
|
|
54
|
+
// pinch
|
|
55
|
+
scaleFactor: z.number().optional().describe('>1 zoom in, <1 zoom out'),
|
|
56
|
+
// Change detection
|
|
57
|
+
detectChanges: z.boolean().optional(),
|
|
58
|
+
settleTimeout: z.number().optional().describe('DOM settle timeout ms'),
|
|
59
|
+
}).strict();
|
|
60
|
+
/**
|
|
61
|
+
* Wrapper to execute input actions while bypassing the replay blocker overlay.
|
|
62
|
+
* Sets __cdpReplayClickInProgress flag before the action and clears it after.
|
|
63
|
+
* This allows CDP-dispatched events to pass through the overlay's event listeners.
|
|
64
|
+
*/
|
|
65
|
+
async function withReplayBypass(page, action) {
|
|
66
|
+
await page.evaluate(() => { globalThis.__cdpReplayClickInProgress = true; });
|
|
67
|
+
try {
|
|
68
|
+
return await action();
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
// Best-effort: never let a failure clearing the flag mask the action's
|
|
72
|
+
// own outcome (in particular an AbortError thrown mid-action).
|
|
73
|
+
try {
|
|
74
|
+
await page.evaluate(() => { globalThis.__cdpReplayClickInProgress = false; });
|
|
75
|
+
}
|
|
76
|
+
catch { /* page may have navigated/closed */ }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Warning text when a selector matches more than one element.
|
|
81
|
+
*
|
|
82
|
+
* The action takes the first match. On a list UI that silently drifts onto the
|
|
83
|
+
* wrong row as the list grows, and the step still reports success - so the
|
|
84
|
+
* count has to be said out loud even though the action succeeded.
|
|
85
|
+
*
|
|
86
|
+
* Advisory only: a selector the browser rejects is the action's error to
|
|
87
|
+
* report, not this helper's.
|
|
88
|
+
*/
|
|
89
|
+
export async function ambiguousSelectorWarning(page, selector, raw) {
|
|
90
|
+
try {
|
|
91
|
+
const count = await page.evaluate((sel) => globalThis.document.querySelectorAll(sel).length, selector);
|
|
92
|
+
if (count > 1) {
|
|
93
|
+
return `\`${raw}\` matched ${count} elements - acted on the first. Narrow it if you meant a specific one.`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Counting is best-effort.
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
export function createInputTools(puppeteerManager, cdpManager, connectionManager, resolveConnectionFromReason) {
|
|
102
|
+
return {
|
|
103
|
+
input: createTool('Perform browser input actions. Actions: click (click element), type (type text into element), press (press keyboard key), hover (hover over element), focus (focus element by selector), focusNext (Tab to next focusable element), focusPrevious (Shift+Tab to previous focusable element), drag (drag from one point to another), scroll (scroll wheel at position), mousemove (move mouse to position), pinch (pinch zoom gesture), tap (real touch tap - selector or x/y), swipe (real touch drag - selector + direction, or raw from/to - for touch-only gestures the mouse cannot drive)', inputToolSchema,
|
|
104
|
+
// abortSignal (#110): input events cannot be recalled once dispatched -
|
|
105
|
+
// Input.dispatchMouseEvent on the wire WILL be processed by Chrome. What
|
|
106
|
+
// cancellation buys here is *not dispatching* events that have not gone
|
|
107
|
+
// out yet: checkpoints sit after connection/selector resolution and
|
|
108
|
+
// immediately before every dispatch, and multi-dispatch paths (Tab
|
|
109
|
+
// loops, drag stepping, clear-and-retype) abort between events. On
|
|
110
|
+
// abort the handler THROWS an abort-shaped error; already-issued
|
|
111
|
+
// dispatches are NOT undone.
|
|
112
|
+
async (args, abortSignal) => {
|
|
113
|
+
const { action, connectionReason } = args;
|
|
114
|
+
throwIfAborted(abortSignal);
|
|
115
|
+
// Resolve connection from reason
|
|
116
|
+
const resolved = await resolveConnectionFromReason(connectionReason);
|
|
117
|
+
if (!resolved) {
|
|
118
|
+
return createErrorResponse('CONNECTION_NOT_FOUND', {
|
|
119
|
+
message: 'No Chrome browser available. Use `launchChrome` first to start a browser.'
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const targetPuppeteerManager = resolved.puppeteerManager || puppeteerManager;
|
|
123
|
+
const targetCdpManager = resolved.cdpManager;
|
|
124
|
+
const error = checkBrowserAutomation(targetCdpManager, targetPuppeteerManager, action, resolved.connection.port);
|
|
125
|
+
if (error) {
|
|
126
|
+
return error;
|
|
127
|
+
}
|
|
128
|
+
const page = targetPuppeteerManager.getPage();
|
|
129
|
+
// Determine if change detection is enabled
|
|
130
|
+
const changeConfig = configManager.getChangeDetectionConfig();
|
|
131
|
+
const shouldDetectChanges = args.detectChanges ?? changeConfig.enabled;
|
|
132
|
+
const settleTimeout = args.settleTimeout ?? changeConfig.settleTimeout;
|
|
133
|
+
// Start observing DOM changes before interaction
|
|
134
|
+
if (shouldDetectChanges && ['click', 'type', 'hover'].includes(action)) {
|
|
135
|
+
await domChangeMonitor.startObserving(connectionReason, page);
|
|
136
|
+
}
|
|
137
|
+
// Abort checkpoint used at every point where nothing (further) has
|
|
138
|
+
// been dispatched yet: tears down the DOM-change observer
|
|
139
|
+
// (best-effort, no settle wait) so a cancelled action does not leave
|
|
140
|
+
// a MutationObserver running in the page, then throws abort-shaped.
|
|
141
|
+
const checkAborted = async () => {
|
|
142
|
+
if (!abortSignal?.aborted)
|
|
143
|
+
return;
|
|
144
|
+
if (shouldDetectChanges && domChangeMonitor.isObserving(connectionReason)) {
|
|
145
|
+
try {
|
|
146
|
+
await domChangeMonitor.stopObserving(connectionReason, { settleTimeout: 0 });
|
|
147
|
+
}
|
|
148
|
+
catch { /* cleanup is best-effort */ }
|
|
149
|
+
}
|
|
150
|
+
throw abortErrorFor(abortSignal);
|
|
151
|
+
};
|
|
152
|
+
await checkAborted();
|
|
153
|
+
switch (action) {
|
|
154
|
+
case 'click': {
|
|
155
|
+
const { selector: rawSelector, clickCount = 1, handleModals = false, dismissStrategy = 'auto', x, y } = args;
|
|
156
|
+
// Coordinate-based click (for canvas/3D apps)
|
|
157
|
+
if (typeof x === 'number' && typeof y === 'number') {
|
|
158
|
+
await checkAborted(); // last exit before the click goes on the wire
|
|
159
|
+
await withReplayBypass(page, () => page.mouse.click(x, y, { clickCount }));
|
|
160
|
+
return {
|
|
161
|
+
content: [{
|
|
162
|
+
type: 'text',
|
|
163
|
+
text: `Clicked at coordinates (${x}, ${y})`
|
|
164
|
+
}]
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (!rawSelector) {
|
|
168
|
+
return {
|
|
169
|
+
content: [
|
|
170
|
+
{
|
|
171
|
+
type: 'text',
|
|
172
|
+
text: `## Error\n\nMissing required parameter: \`selector\` or coordinates (\`x\`, \`y\`)\n\n**Action:** click\n\n**Suggestion:** Provide a CSS selector for the element to click, or x/y coordinates for coordinate-based clicking.`,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
isError: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// Capture pre-click URL for validation metadata
|
|
179
|
+
const preClickUrl = page.url();
|
|
180
|
+
// Resolve extended selectors (like :has-text())
|
|
181
|
+
let selector = rawSelector;
|
|
182
|
+
let selectorWarning;
|
|
183
|
+
if (isExtendedSelector(rawSelector)) {
|
|
184
|
+
const resolved = await resolveSelector(page, rawSelector);
|
|
185
|
+
if ('error' in resolved) {
|
|
186
|
+
return createErrorResponse('ELEMENT_NOT_FOUND', {
|
|
187
|
+
selector: rawSelector,
|
|
188
|
+
suggestion: resolved.suggestion,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
selector = resolved.selector;
|
|
192
|
+
selectorWarning = resolved.warning;
|
|
193
|
+
}
|
|
194
|
+
{
|
|
195
|
+
const ambiguous = await ambiguousSelectorWarning(page, selector, rawSelector);
|
|
196
|
+
if (ambiguous)
|
|
197
|
+
selectorWarning = selectorWarning ? `${selectorWarning} ${ambiguous}` : ambiguous;
|
|
198
|
+
}
|
|
199
|
+
await checkAborted(); // after selector resolution, before any dispatch
|
|
200
|
+
const result = await executeWithPauseDetection(targetCdpManager, async () => {
|
|
201
|
+
// Check if element exists and is clickable
|
|
202
|
+
const element = await page.$(selector);
|
|
203
|
+
if (!element) {
|
|
204
|
+
return {
|
|
205
|
+
error: `Element not found: ${selector}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// Check if element is blocked by modal
|
|
209
|
+
const blockingCheck = await isElementBlocked(page, selector);
|
|
210
|
+
if (blockingCheck.blocked && blockingCheck.blockingModal) {
|
|
211
|
+
if (handleModals) {
|
|
212
|
+
// Auto-dismiss modal. Dismissal DISPATCHES too (an Escape
|
|
213
|
+
// key or a click on the dismiss control), so it needs its
|
|
214
|
+
// own pre-dispatch checkpoint - not just the one before the
|
|
215
|
+
// real action below.
|
|
216
|
+
await checkAborted();
|
|
217
|
+
const dismissResult = await dismissModalHelper(page, blockingCheck.blockingModal.selector, dismissStrategy);
|
|
218
|
+
// Check if dismissal was successful
|
|
219
|
+
if (!dismissResult.success) {
|
|
220
|
+
return {
|
|
221
|
+
error: `Failed to dismiss blocking modal: ${dismissResult.error || 'Unknown error'}`,
|
|
222
|
+
blockingModal: blockingCheck.blockingModal,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// Re-check if element is still blocked
|
|
226
|
+
const recheckBlocking = await isElementBlocked(page, selector);
|
|
227
|
+
if (recheckBlocking.blocked) {
|
|
228
|
+
return {
|
|
229
|
+
error: `Element still blocked after dismissing modal`,
|
|
230
|
+
blockingModal: recheckBlocking.blockingModal,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// Return error with modal information
|
|
236
|
+
return {
|
|
237
|
+
error: `Element is blocked by modal`,
|
|
238
|
+
blockingModal: blockingCheck.blockingModal,
|
|
239
|
+
suggestion: `Enable handleModals parameter or call dismissModal tool first`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Check if element has click handlers
|
|
244
|
+
const hasClickHandler = await page.evaluate((sel) => {
|
|
245
|
+
const el = globalThis.document.querySelector(sel);
|
|
246
|
+
if (!el)
|
|
247
|
+
return false;
|
|
248
|
+
// Check for onclick attribute
|
|
249
|
+
if (el.onclick)
|
|
250
|
+
return true;
|
|
251
|
+
// Check for addEventListener listeners (limited - can't detect all)
|
|
252
|
+
// Check if element or ancestors have event listeners by testing common patterns
|
|
253
|
+
let current = el;
|
|
254
|
+
while (current) {
|
|
255
|
+
// Check for common click-related attributes
|
|
256
|
+
if (current.hasAttribute('onclick'))
|
|
257
|
+
return true;
|
|
258
|
+
if (current.hasAttribute('data-action'))
|
|
259
|
+
return true;
|
|
260
|
+
// Check for interactive elements that typically have handlers
|
|
261
|
+
const tag = current.tagName.toLowerCase();
|
|
262
|
+
if (tag === 'button' || tag === 'a' || tag === 'input')
|
|
263
|
+
return true;
|
|
264
|
+
// Check for cursor pointer (often indicates clickable)
|
|
265
|
+
const style = globalThis.window.getComputedStyle(current);
|
|
266
|
+
if (style.cursor === 'pointer')
|
|
267
|
+
return true;
|
|
268
|
+
current = current.parentElement;
|
|
269
|
+
}
|
|
270
|
+
return false;
|
|
271
|
+
}, selector);
|
|
272
|
+
// Perform the click - use wrapper to bypass replay blocker overlay
|
|
273
|
+
await checkAborted(); // last exit before the click goes on the wire
|
|
274
|
+
await withReplayBypass(page, () => page.click(selector, { clickCount }));
|
|
275
|
+
// Check if breakpoint was hit during click - if so, skip post-click evaluation
|
|
276
|
+
// which would hang because page JS is paused
|
|
277
|
+
if (targetCdpManager.isPaused()) {
|
|
278
|
+
return {
|
|
279
|
+
selector,
|
|
280
|
+
clickCount,
|
|
281
|
+
hasClickHandler,
|
|
282
|
+
postClickState: null,
|
|
283
|
+
pausedDuringClick: true,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
// Get post-click state
|
|
287
|
+
const postClickState = await page.evaluate((clickedSelector) => {
|
|
288
|
+
const focused = globalThis.document.activeElement;
|
|
289
|
+
let focusInfo = null;
|
|
290
|
+
if (focused && focused !== globalThis.document.body) {
|
|
291
|
+
const tag = focused.tagName.toLowerCase();
|
|
292
|
+
const text = focused.textContent?.trim().substring(0, 30) || '';
|
|
293
|
+
const ariaLabel = focused.getAttribute('aria-label') || '';
|
|
294
|
+
const placeholder = focused.getAttribute('placeholder') || '';
|
|
295
|
+
const type = focused.getAttribute('type') || '';
|
|
296
|
+
focusInfo = {
|
|
297
|
+
tag,
|
|
298
|
+
type: type || undefined,
|
|
299
|
+
text: text || ariaLabel || placeholder || undefined,
|
|
300
|
+
isInput: ['input', 'textarea', 'select'].includes(tag),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// Get tabbable elements
|
|
304
|
+
const tabbable = Array.from(globalThis.document.querySelectorAll('a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])')).filter((el) => {
|
|
305
|
+
const style = globalThis.window.getComputedStyle(el);
|
|
306
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && !el.disabled;
|
|
307
|
+
});
|
|
308
|
+
// Helper to format element for display
|
|
309
|
+
const formatEl = (el) => {
|
|
310
|
+
const tag = el.tagName.toLowerCase();
|
|
311
|
+
const text = el.textContent?.trim().substring(0, 20) || el.getAttribute('aria-label') || el.getAttribute('placeholder') || '';
|
|
312
|
+
const type = el.getAttribute('type');
|
|
313
|
+
if (tag === 'input') {
|
|
314
|
+
return `${tag}[${type || 'text'}]${text ? ` "${text}"` : ''}`;
|
|
315
|
+
}
|
|
316
|
+
return `${tag}${text ? ` "${text}"` : ''}`;
|
|
317
|
+
};
|
|
318
|
+
let prevTabbable = [];
|
|
319
|
+
let nextTabbable = [];
|
|
320
|
+
if (focused) {
|
|
321
|
+
const currentIndex = tabbable.indexOf(focused);
|
|
322
|
+
if (currentIndex !== -1) {
|
|
323
|
+
// Previous 5 tabbable elements
|
|
324
|
+
prevTabbable = tabbable.slice(Math.max(0, currentIndex - 5), currentIndex).map(formatEl);
|
|
325
|
+
// Next 5 tabbable elements
|
|
326
|
+
nextTabbable = tabbable.slice(currentIndex + 1, currentIndex + 6).map(formatEl);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Get child interactive elements of clicked element
|
|
330
|
+
let childInteractive = [];
|
|
331
|
+
try {
|
|
332
|
+
const clickedEl = globalThis.document.querySelector(clickedSelector);
|
|
333
|
+
if (clickedEl) {
|
|
334
|
+
const children = clickedEl.querySelectorAll('a[href], button, input, select, textarea');
|
|
335
|
+
childInteractive = Array.from(children).slice(0, 10).map((el) => formatEl(el));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
// Selector may have been cleaned up, ignore
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
focusInfo,
|
|
343
|
+
prevTabbable,
|
|
344
|
+
nextTabbable,
|
|
345
|
+
childInteractive,
|
|
346
|
+
url: globalThis.window.location.href,
|
|
347
|
+
};
|
|
348
|
+
}, selector);
|
|
349
|
+
return {
|
|
350
|
+
selector,
|
|
351
|
+
clickCount,
|
|
352
|
+
hasClickHandler,
|
|
353
|
+
postClickState,
|
|
354
|
+
warning: !hasClickHandler ? 'Element may not have a click handler attached. Click was performed but may not trigger any action.' : undefined,
|
|
355
|
+
};
|
|
356
|
+
}, 'click');
|
|
357
|
+
// If paused at breakpoint, return immediately - don't try any more page interactions
|
|
358
|
+
if (result.pausedAtBreakpoint || result.result?.pausedDuringClick) {
|
|
359
|
+
// Stop observing without waiting
|
|
360
|
+
if (shouldDetectChanges) {
|
|
361
|
+
await domChangeMonitor.stopObserving(connectionReason, { settleTimeout: 0 });
|
|
362
|
+
}
|
|
363
|
+
// Get pause info if we detected pause inside the action
|
|
364
|
+
const pauseInfo = result.pauseInfo || (result.result?.pausedDuringClick ? (() => {
|
|
365
|
+
const info = targetCdpManager.getPausedInfo();
|
|
366
|
+
return info.location ? {
|
|
367
|
+
url: info.location.url,
|
|
368
|
+
lineNumber: info.location.lineNumber,
|
|
369
|
+
} : undefined;
|
|
370
|
+
})() : undefined);
|
|
371
|
+
return createSuccessResponse('ACTION_PAUSED_AT_BREAKPOINT', {
|
|
372
|
+
action: 'click',
|
|
373
|
+
selector: rawSelector,
|
|
374
|
+
...pauseInfo,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
// Collect DOM changes
|
|
378
|
+
let changesText = '';
|
|
379
|
+
let changes = null;
|
|
380
|
+
if (shouldDetectChanges) {
|
|
381
|
+
changes = await domChangeMonitor.stopObserving(connectionReason, { settleTimeout, signal: abortSignal });
|
|
382
|
+
changesText = formatDOMChanges(changes);
|
|
383
|
+
}
|
|
384
|
+
// Clean up temporary selector attribute
|
|
385
|
+
await cleanupResolvedSelector(page, selector);
|
|
386
|
+
// Check if element was not found
|
|
387
|
+
if (!result.result || result.result.error) {
|
|
388
|
+
// Check if error is due to blocking modal
|
|
389
|
+
if (result.result?.blockingModal) {
|
|
390
|
+
return createErrorResponse('ELEMENT_BLOCKED_BY_MODAL', {
|
|
391
|
+
selector: rawSelector,
|
|
392
|
+
modalType: result.result.blockingModal.type,
|
|
393
|
+
modalDescription: result.result.blockingModal.description,
|
|
394
|
+
modalSelector: result.result.blockingModal.selector,
|
|
395
|
+
suggestion: result.result.suggestion,
|
|
396
|
+
availableStrategies: result.result.blockingModal.dismissStrategies,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return createErrorResponse('ELEMENT_NOT_FOUND', { selector: rawSelector });
|
|
400
|
+
}
|
|
401
|
+
// Build post-click info string
|
|
402
|
+
const postClick = result.result.postClickState;
|
|
403
|
+
let postClickInfo = '';
|
|
404
|
+
if (postClick?.focusInfo) {
|
|
405
|
+
const f = postClick.focusInfo;
|
|
406
|
+
if (f.isInput) {
|
|
407
|
+
postClickInfo = `\n**Focus:** ${f.tag}${f.type ? `[type="${f.type}"]` : ''} ${f.text ? `"${f.text}"` : ''}`;
|
|
408
|
+
}
|
|
409
|
+
else if (f.tag !== 'body') {
|
|
410
|
+
postClickInfo = `\n**Focus:** ${f.tag}${f.text ? ` "${f.text}"` : ''}`;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (postClick?.prevTabbable?.length > 0) {
|
|
414
|
+
postClickInfo += `\n**Prev tab:** ${postClick.prevTabbable.join(' ← ')}`;
|
|
415
|
+
}
|
|
416
|
+
if (postClick?.nextTabbable?.length > 0) {
|
|
417
|
+
postClickInfo += `\n**Next tab:** ${postClick.nextTabbable.join(' → ')}`;
|
|
418
|
+
}
|
|
419
|
+
if (postClick?.childInteractive?.length > 0) {
|
|
420
|
+
postClickInfo += `\n**Contains:** ${postClick.childInteractive.join(', ')}`;
|
|
421
|
+
}
|
|
422
|
+
// Build _meta for click validation
|
|
423
|
+
const postClickUrl = postClick?.url || page.url();
|
|
424
|
+
const clickMeta = {
|
|
425
|
+
tool: 'input',
|
|
426
|
+
action: 'click',
|
|
427
|
+
timestamp: Date.now(),
|
|
428
|
+
click: {
|
|
429
|
+
selector: rawSelector,
|
|
430
|
+
preClickUrl,
|
|
431
|
+
postClickUrl,
|
|
432
|
+
navigationOccurred: preClickUrl !== postClickUrl,
|
|
433
|
+
hasClickHandler: result.result.hasClickHandler ?? false,
|
|
434
|
+
domChanges: changes ? {
|
|
435
|
+
mutationCount: changes.mutationCount,
|
|
436
|
+
added: changes.added?.length || 0,
|
|
437
|
+
removed: changes.removed?.length || 0,
|
|
438
|
+
shown: changes.shown?.length || 0,
|
|
439
|
+
hidden: changes.hidden?.length || 0,
|
|
440
|
+
} : null,
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
// Return success with warning if no click handler detected
|
|
444
|
+
if (result.result.warning) {
|
|
445
|
+
const response = createSuccessResponse('ELEMENT_CLICK_WARNING', { selector: rawSelector, warning: selectorWarning });
|
|
446
|
+
response._meta = clickMeta;
|
|
447
|
+
return response;
|
|
448
|
+
}
|
|
449
|
+
// Include warning about multiple matches if applicable
|
|
450
|
+
if (selectorWarning) {
|
|
451
|
+
return {
|
|
452
|
+
content: [
|
|
453
|
+
{
|
|
454
|
+
type: 'text',
|
|
455
|
+
text: `Clicked element \`${rawSelector}\`${changesText}${postClickInfo}\n\n**Warning:** ${selectorWarning}`,
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
_meta: clickMeta,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
// Default success response with post-click info and changes
|
|
462
|
+
return {
|
|
463
|
+
content: [
|
|
464
|
+
{
|
|
465
|
+
type: 'text',
|
|
466
|
+
text: `Clicked element: \`${rawSelector}\`${changesText}${postClickInfo}`,
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
_meta: clickMeta,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
case 'type': {
|
|
473
|
+
const { selector: rawSelector, text, delay = 0, handleModals = false, dismissStrategy = 'auto', append = false } = args;
|
|
474
|
+
if (!rawSelector) {
|
|
475
|
+
return {
|
|
476
|
+
content: [
|
|
477
|
+
{
|
|
478
|
+
type: 'text',
|
|
479
|
+
text: `## Error\n\nMissing required parameter: \`selector\`\n\n**Action:** type\n\n**Suggestion:** Provide a CSS selector for the input element.`,
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
isError: true,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (!text) {
|
|
486
|
+
return {
|
|
487
|
+
content: [
|
|
488
|
+
{
|
|
489
|
+
type: 'text',
|
|
490
|
+
text: `## Error\n\nMissing required parameter: \`text\`\n\n**Action:** type\n\n**Suggestion:** Provide text to type into the element.`,
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
isError: true,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
// Resolve extended selectors (like :has-text())
|
|
497
|
+
let selector = rawSelector;
|
|
498
|
+
let selectorWarning;
|
|
499
|
+
if (isExtendedSelector(rawSelector)) {
|
|
500
|
+
const resolved = await resolveSelector(page, rawSelector);
|
|
501
|
+
if ('error' in resolved) {
|
|
502
|
+
return createErrorResponse('ELEMENT_NOT_FOUND', {
|
|
503
|
+
selector: rawSelector,
|
|
504
|
+
suggestion: resolved.suggestion,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
selector = resolved.selector;
|
|
508
|
+
selectorWarning = resolved.warning;
|
|
509
|
+
}
|
|
510
|
+
{
|
|
511
|
+
const ambiguous = await ambiguousSelectorWarning(page, selector, rawSelector);
|
|
512
|
+
if (ambiguous)
|
|
513
|
+
selectorWarning = selectorWarning ? `${selectorWarning} ${ambiguous}` : ambiguous;
|
|
514
|
+
}
|
|
515
|
+
await checkAborted(); // after selector resolution, before any dispatch
|
|
516
|
+
const result = await executeWithPauseDetection(targetCdpManager, async () => {
|
|
517
|
+
// Check if element exists first
|
|
518
|
+
const element = await page.$(selector);
|
|
519
|
+
if (!element) {
|
|
520
|
+
return { error: `Element not found: ${selector}` };
|
|
521
|
+
}
|
|
522
|
+
// Check if element is blocked by modal
|
|
523
|
+
const blockingCheck = await isElementBlocked(page, selector);
|
|
524
|
+
if (blockingCheck.blocked && blockingCheck.blockingModal) {
|
|
525
|
+
if (handleModals) {
|
|
526
|
+
// Auto-dismiss modal. Dismissal DISPATCHES too (an Escape
|
|
527
|
+
// key or a click on the dismiss control), so it needs its
|
|
528
|
+
// own pre-dispatch checkpoint - not just the one before the
|
|
529
|
+
// real action below.
|
|
530
|
+
await checkAborted();
|
|
531
|
+
const dismissResult = await dismissModalHelper(page, blockingCheck.blockingModal.selector, dismissStrategy);
|
|
532
|
+
// Check if dismissal was successful
|
|
533
|
+
if (!dismissResult.success) {
|
|
534
|
+
return {
|
|
535
|
+
error: `Failed to dismiss blocking modal: ${dismissResult.error || 'Unknown error'}`,
|
|
536
|
+
blockingModal: blockingCheck.blockingModal,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
// Re-check if element is still blocked
|
|
540
|
+
const recheckBlocking = await isElementBlocked(page, selector);
|
|
541
|
+
if (recheckBlocking.blocked) {
|
|
542
|
+
return {
|
|
543
|
+
error: `Element still blocked after dismissing modal`,
|
|
544
|
+
blockingModal: recheckBlocking.blockingModal,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
// Return error with modal information
|
|
550
|
+
return {
|
|
551
|
+
error: `Element is blocked by modal`,
|
|
552
|
+
blockingModal: blockingCheck.blockingModal,
|
|
553
|
+
suggestion: `Enable handleModals parameter or call dismissModal tool first`,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// Clear existing text first (unless append mode)
|
|
558
|
+
await checkAborted(); // last exit before keystrokes go on the wire
|
|
559
|
+
if (!append) {
|
|
560
|
+
await withReplayBypass(page, async () => {
|
|
561
|
+
await page.click(selector, { clickCount: 3 });
|
|
562
|
+
await page.keyboard.press('Backspace');
|
|
563
|
+
});
|
|
564
|
+
// Abortable between clear and retype: the clear that went
|
|
565
|
+
// out stays out, but the new text is not dispatched.
|
|
566
|
+
await checkAborted();
|
|
567
|
+
}
|
|
568
|
+
// Type new text - use wrapper to bypass replay blocker overlay
|
|
569
|
+
await withReplayBypass(page, () => page.type(selector, text, { delay }));
|
|
570
|
+
// Get the actual value after typing
|
|
571
|
+
const currentValue = await page.$eval(selector, (el) => {
|
|
572
|
+
const element = el;
|
|
573
|
+
if ('value' in element && element.value !== undefined) {
|
|
574
|
+
return element.value;
|
|
575
|
+
}
|
|
576
|
+
return element.textContent || '';
|
|
577
|
+
});
|
|
578
|
+
return { selector, text, currentValue };
|
|
579
|
+
}, 'typeText');
|
|
580
|
+
// If paused at breakpoint, return immediately - don't try any more page interactions
|
|
581
|
+
if (result.pausedAtBreakpoint) {
|
|
582
|
+
if (shouldDetectChanges) {
|
|
583
|
+
await domChangeMonitor.stopObserving(connectionReason, { settleTimeout: 0 });
|
|
584
|
+
}
|
|
585
|
+
return createSuccessResponse('ACTION_PAUSED_AT_BREAKPOINT', {
|
|
586
|
+
action: 'type',
|
|
587
|
+
selector: rawSelector,
|
|
588
|
+
...result.pauseInfo,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
// Collect DOM changes
|
|
592
|
+
let changesText = '';
|
|
593
|
+
if (shouldDetectChanges) {
|
|
594
|
+
const changes = await domChangeMonitor.stopObserving(connectionReason, { settleTimeout, signal: abortSignal });
|
|
595
|
+
changesText = formatDOMChanges(changes);
|
|
596
|
+
}
|
|
597
|
+
// Clean up temporary selector attribute
|
|
598
|
+
await cleanupResolvedSelector(page, selector);
|
|
599
|
+
// Check if element was not found
|
|
600
|
+
if (result.result?.error) {
|
|
601
|
+
// Check if error is due to blocking modal
|
|
602
|
+
if (result.result?.blockingModal) {
|
|
603
|
+
return createErrorResponse('ELEMENT_BLOCKED_BY_MODAL', {
|
|
604
|
+
selector: rawSelector,
|
|
605
|
+
modalType: result.result.blockingModal.type,
|
|
606
|
+
modalDescription: result.result.blockingModal.description,
|
|
607
|
+
modalSelector: result.result.blockingModal.selector,
|
|
608
|
+
suggestion: result.result.suggestion,
|
|
609
|
+
availableStrategies: result.result.blockingModal.dismissStrategies,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
return createErrorResponse('ELEMENT_NOT_FOUND', { selector: rawSelector });
|
|
613
|
+
}
|
|
614
|
+
// Include warning about multiple matches if applicable
|
|
615
|
+
if (selectorWarning) {
|
|
616
|
+
return {
|
|
617
|
+
content: [
|
|
618
|
+
{
|
|
619
|
+
type: 'text',
|
|
620
|
+
text: `Typed into element \`${rawSelector}\`${changesText}\n\nCurrent value: ${result.result?.currentValue}\n\n**Warning:** ${selectorWarning}`,
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
// Default success response with changes
|
|
626
|
+
return {
|
|
627
|
+
content: [
|
|
628
|
+
{
|
|
629
|
+
type: 'text',
|
|
630
|
+
text: `Text typed into \`${rawSelector}\`: "${text}"${changesText}`,
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
case 'press': {
|
|
636
|
+
const { key } = args;
|
|
637
|
+
if (!key) {
|
|
638
|
+
return {
|
|
639
|
+
content: [
|
|
640
|
+
{
|
|
641
|
+
type: 'text',
|
|
642
|
+
text: `## Error\n\nMissing required parameter: \`key\`\n\n**Action:** press\n\n**Suggestion:** Provide a key name to press (e.g., "Enter", "Tab", "Escape").`,
|
|
643
|
+
},
|
|
644
|
+
],
|
|
645
|
+
isError: true,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
// Use wrapper to bypass replay blocker overlay for key press
|
|
649
|
+
await checkAborted(); // last exit before the key press goes on the wire
|
|
650
|
+
await withReplayBypass(page, () => executeWithPauseDetection(targetCdpManager, () => page.keyboard.press(key), 'pressKey'));
|
|
651
|
+
return createSuccessResponse('KEY_PRESS_SUCCESS', {
|
|
652
|
+
key
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
case 'hover': {
|
|
656
|
+
const { selector: rawSelector, handleModals = false, dismissStrategy = 'auto' } = args;
|
|
657
|
+
if (!rawSelector) {
|
|
658
|
+
return {
|
|
659
|
+
content: [
|
|
660
|
+
{
|
|
661
|
+
type: 'text',
|
|
662
|
+
text: `## Error\n\nMissing required parameter: \`selector\`\n\n**Action:** hover\n\n**Suggestion:** Provide a CSS selector for the element to hover over.`,
|
|
663
|
+
},
|
|
664
|
+
],
|
|
665
|
+
isError: true,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
// Resolve extended selectors (like :has-text())
|
|
669
|
+
let selector = rawSelector;
|
|
670
|
+
let selectorWarning;
|
|
671
|
+
if (isExtendedSelector(rawSelector)) {
|
|
672
|
+
const resolved = await resolveSelector(page, rawSelector);
|
|
673
|
+
if ('error' in resolved) {
|
|
674
|
+
return createErrorResponse('ELEMENT_NOT_FOUND', {
|
|
675
|
+
selector: rawSelector,
|
|
676
|
+
suggestion: resolved.suggestion,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
selector = resolved.selector;
|
|
680
|
+
selectorWarning = resolved.warning;
|
|
681
|
+
}
|
|
682
|
+
{
|
|
683
|
+
const ambiguous = await ambiguousSelectorWarning(page, selector, rawSelector);
|
|
684
|
+
if (ambiguous)
|
|
685
|
+
selectorWarning = selectorWarning ? `${selectorWarning} ${ambiguous}` : ambiguous;
|
|
686
|
+
}
|
|
687
|
+
await checkAborted(); // after selector resolution, before any dispatch
|
|
688
|
+
const result = await executeWithPauseDetection(targetCdpManager, async () => {
|
|
689
|
+
// Check if element exists first
|
|
690
|
+
const element = await page.$(selector);
|
|
691
|
+
if (!element) {
|
|
692
|
+
return { error: `Element not found: ${selector}` };
|
|
693
|
+
}
|
|
694
|
+
// Check if element is blocked by modal
|
|
695
|
+
const blockingCheck = await isElementBlocked(page, selector);
|
|
696
|
+
if (blockingCheck.blocked && blockingCheck.blockingModal) {
|
|
697
|
+
if (handleModals) {
|
|
698
|
+
// Auto-dismiss modal. Dismissal DISPATCHES too (an Escape
|
|
699
|
+
// key or a click on the dismiss control), so it needs its
|
|
700
|
+
// own pre-dispatch checkpoint - not just the one before the
|
|
701
|
+
// real action below.
|
|
702
|
+
await checkAborted();
|
|
703
|
+
const dismissResult = await dismissModalHelper(page, blockingCheck.blockingModal.selector, dismissStrategy);
|
|
704
|
+
// Check if dismissal was successful
|
|
705
|
+
if (!dismissResult.success) {
|
|
706
|
+
return {
|
|
707
|
+
error: `Failed to dismiss blocking modal: ${dismissResult.error || 'Unknown error'}`,
|
|
708
|
+
blockingModal: blockingCheck.blockingModal,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
// Re-check if element is still blocked
|
|
712
|
+
const recheckBlocking = await isElementBlocked(page, selector);
|
|
713
|
+
if (recheckBlocking.blocked) {
|
|
714
|
+
return {
|
|
715
|
+
error: `Element still blocked after dismissing modal`,
|
|
716
|
+
blockingModal: recheckBlocking.blockingModal,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
// Return error with modal information
|
|
722
|
+
return {
|
|
723
|
+
error: `Element is blocked by modal`,
|
|
724
|
+
blockingModal: blockingCheck.blockingModal,
|
|
725
|
+
suggestion: `Enable handleModals parameter or call dismissModal tool first`,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
await checkAborted(); // last exit before the hover goes on the wire
|
|
730
|
+
await withReplayBypass(page, () => page.hover(selector));
|
|
731
|
+
return { selector };
|
|
732
|
+
}, 'hoverElement');
|
|
733
|
+
// If paused at breakpoint, return immediately - don't try any more page interactions
|
|
734
|
+
if (result.pausedAtBreakpoint) {
|
|
735
|
+
if (shouldDetectChanges) {
|
|
736
|
+
await domChangeMonitor.stopObserving(connectionReason, { settleTimeout: 0 });
|
|
737
|
+
}
|
|
738
|
+
return createSuccessResponse('ACTION_PAUSED_AT_BREAKPOINT', {
|
|
739
|
+
action: 'hover',
|
|
740
|
+
selector: rawSelector,
|
|
741
|
+
...result.pauseInfo,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
// Collect DOM changes
|
|
745
|
+
let changesText = '';
|
|
746
|
+
if (shouldDetectChanges) {
|
|
747
|
+
const changes = await domChangeMonitor.stopObserving(connectionReason, { settleTimeout, signal: abortSignal });
|
|
748
|
+
changesText = formatDOMChanges(changes);
|
|
749
|
+
}
|
|
750
|
+
// Clean up temporary selector attribute
|
|
751
|
+
await cleanupResolvedSelector(page, selector);
|
|
752
|
+
// Check if element was not found
|
|
753
|
+
if (result.result?.error) {
|
|
754
|
+
// Check if error is due to blocking modal
|
|
755
|
+
if (result.result?.blockingModal) {
|
|
756
|
+
return createErrorResponse('ELEMENT_BLOCKED_BY_MODAL', {
|
|
757
|
+
selector: rawSelector,
|
|
758
|
+
modalType: result.result.blockingModal.type,
|
|
759
|
+
modalDescription: result.result.blockingModal.description,
|
|
760
|
+
modalSelector: result.result.blockingModal.selector,
|
|
761
|
+
suggestion: result.result.suggestion,
|
|
762
|
+
availableStrategies: result.result.blockingModal.dismissStrategies,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
return createErrorResponse('ELEMENT_NOT_FOUND', { selector: rawSelector });
|
|
766
|
+
}
|
|
767
|
+
// Include warning about multiple matches if applicable
|
|
768
|
+
if (selectorWarning) {
|
|
769
|
+
return {
|
|
770
|
+
content: [
|
|
771
|
+
{
|
|
772
|
+
type: 'text',
|
|
773
|
+
text: `Hovered over element \`${rawSelector}\`${changesText}\n\n**Warning:** ${selectorWarning}`,
|
|
774
|
+
},
|
|
775
|
+
],
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
// Default success response with changes
|
|
779
|
+
return {
|
|
780
|
+
content: [
|
|
781
|
+
{
|
|
782
|
+
type: 'text',
|
|
783
|
+
text: `Hovered over element: \`${rawSelector}\`${changesText}`,
|
|
784
|
+
},
|
|
785
|
+
],
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
case 'focus': {
|
|
789
|
+
const { selector: rawSelector } = args;
|
|
790
|
+
if (!rawSelector) {
|
|
791
|
+
return {
|
|
792
|
+
content: [
|
|
793
|
+
{
|
|
794
|
+
type: 'text',
|
|
795
|
+
text: `## Error\n\nMissing required parameter: \`selector\`\n\n**Action:** focus\n\n**Suggestion:** Provide a CSS selector for the element to focus.`,
|
|
796
|
+
},
|
|
797
|
+
],
|
|
798
|
+
isError: true,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
// Resolve extended selectors (like :has-text())
|
|
802
|
+
let selector = rawSelector;
|
|
803
|
+
let selectorWarning;
|
|
804
|
+
if (isExtendedSelector(rawSelector)) {
|
|
805
|
+
const resolved = await resolveSelector(page, rawSelector);
|
|
806
|
+
if ('error' in resolved) {
|
|
807
|
+
return createErrorResponse('ELEMENT_NOT_FOUND', {
|
|
808
|
+
selector: rawSelector,
|
|
809
|
+
suggestion: resolved.suggestion,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
selector = resolved.selector;
|
|
813
|
+
selectorWarning = resolved.warning;
|
|
814
|
+
}
|
|
815
|
+
{
|
|
816
|
+
const ambiguous = await ambiguousSelectorWarning(page, selector, rawSelector);
|
|
817
|
+
if (ambiguous)
|
|
818
|
+
selectorWarning = selectorWarning ? `${selectorWarning} ${ambiguous}` : ambiguous;
|
|
819
|
+
}
|
|
820
|
+
const result = await executeWithPauseDetection(targetCdpManager, async () => {
|
|
821
|
+
// Check if element exists first
|
|
822
|
+
const element = await page.$(selector);
|
|
823
|
+
if (!element) {
|
|
824
|
+
return { error: `Element not found: ${selector}` };
|
|
825
|
+
}
|
|
826
|
+
// Focus the element
|
|
827
|
+
await checkAborted(); // last exit before the focus is dispatched
|
|
828
|
+
await withReplayBypass(page, () => page.focus(selector));
|
|
829
|
+
// Get focused element info
|
|
830
|
+
const focusInfo = await getFocusedElementInfo(page);
|
|
831
|
+
return { selector, focusInfo };
|
|
832
|
+
}, 'focus');
|
|
833
|
+
// Clean up temporary selector attribute
|
|
834
|
+
await cleanupResolvedSelector(page, selector);
|
|
835
|
+
if (result.result?.error) {
|
|
836
|
+
return createErrorResponse('ELEMENT_NOT_FOUND', { selector: rawSelector });
|
|
837
|
+
}
|
|
838
|
+
const focusInfo = result.result?.focusInfo;
|
|
839
|
+
return createSuccessResponse('ELEMENT_FOCUS_SUCCESS', {
|
|
840
|
+
description: focusInfo?.description || 'Unknown element',
|
|
841
|
+
selector: focusInfo?.selector || rawSelector,
|
|
842
|
+
nextTabbable: focusInfo?.nextTabbable?.length ? focusInfo.nextTabbable.join(' → ') : undefined,
|
|
843
|
+
warning: selectorWarning || undefined
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
case 'focusNext': {
|
|
847
|
+
const { count = 1 } = args;
|
|
848
|
+
const result = await executeWithPauseDetection(targetCdpManager, async () => {
|
|
849
|
+
// Press Tab count times - use wrapper to bypass replay blocker overlay
|
|
850
|
+
await withReplayBypass(page, async () => {
|
|
851
|
+
for (let i = 0; i < count; i++) {
|
|
852
|
+
// Abortable between Tabs: dispatched presses stay
|
|
853
|
+
// dispatched, but no further ones go out.
|
|
854
|
+
throwIfAborted(abortSignal);
|
|
855
|
+
await page.keyboard.press('Tab');
|
|
856
|
+
// Small delay between tabs for stability
|
|
857
|
+
if (i < count - 1) {
|
|
858
|
+
await abortableSleep(50, abortSignal);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
// Get focused element info
|
|
863
|
+
const focusInfo = await getFocusedElementInfo(page);
|
|
864
|
+
return { focusInfo, tabCount: count };
|
|
865
|
+
}, 'focusNext');
|
|
866
|
+
const focusInfo = result.result?.focusInfo;
|
|
867
|
+
return createSuccessResponse('FOCUS_NEXT_SUCCESS', {
|
|
868
|
+
count: count > 1 ? count : undefined,
|
|
869
|
+
description: focusInfo?.description || 'No element focused (may have reached end of page)',
|
|
870
|
+
selector: focusInfo?.selector || 'none',
|
|
871
|
+
nextTabbable: focusInfo?.nextTabbable?.length ? focusInfo.nextTabbable.join(' → ') : undefined
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
case 'focusPrevious': {
|
|
875
|
+
const { count = 1 } = args;
|
|
876
|
+
const result = await executeWithPauseDetection(targetCdpManager, async () => {
|
|
877
|
+
// Press Shift+Tab count times - use wrapper to bypass replay blocker overlay
|
|
878
|
+
await withReplayBypass(page, async () => {
|
|
879
|
+
for (let i = 0; i < count; i++) {
|
|
880
|
+
// Abortable between Tabs (not mid-chord: Shift down/Tab/
|
|
881
|
+
// Shift up always complete together so no modifier is
|
|
882
|
+
// left held down).
|
|
883
|
+
throwIfAborted(abortSignal);
|
|
884
|
+
await page.keyboard.down('Shift');
|
|
885
|
+
await page.keyboard.press('Tab');
|
|
886
|
+
await page.keyboard.up('Shift');
|
|
887
|
+
// Small delay between tabs for stability
|
|
888
|
+
if (i < count - 1) {
|
|
889
|
+
await abortableSleep(50, abortSignal);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
// Get focused element info
|
|
894
|
+
const focusInfo = await getFocusedElementInfo(page);
|
|
895
|
+
return { focusInfo, tabCount: count };
|
|
896
|
+
}, 'focusPrevious');
|
|
897
|
+
const focusInfo = result.result?.focusInfo;
|
|
898
|
+
return createSuccessResponse('FOCUS_PREVIOUS_SUCCESS', {
|
|
899
|
+
count: count > 1 ? count : undefined,
|
|
900
|
+
description: focusInfo?.description || 'No element focused (may have reached start of page)',
|
|
901
|
+
selector: focusInfo?.selector || 'none',
|
|
902
|
+
nextTabbable: focusInfo?.nextTabbable?.length ? focusInfo.nextTabbable.join(' → ') : undefined
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
case 'drag': {
|
|
906
|
+
const { from, to, steps = 10 } = args;
|
|
907
|
+
if (!from || !to) {
|
|
908
|
+
return {
|
|
909
|
+
content: [
|
|
910
|
+
{
|
|
911
|
+
type: 'text',
|
|
912
|
+
text: `## Error\n\nMissing required parameters: \`from\` and \`to\`\n\n**Action:** drag\n\n**Suggestion:** Provide starting and ending coordinates. Example: from: {x: 100, y: 100}, to: {x: 200, y: 200}`,
|
|
913
|
+
},
|
|
914
|
+
],
|
|
915
|
+
isError: true,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
const result = await executeWithPauseDetection(targetCdpManager, async () => {
|
|
919
|
+
// Use wrapper to bypass replay blocker overlay for entire drag operation
|
|
920
|
+
return await withReplayBypass(page, async () => {
|
|
921
|
+
const mouse = page.mouse;
|
|
922
|
+
// Abortable before anything is dispatched.
|
|
923
|
+
throwIfAborted(abortSignal);
|
|
924
|
+
// Move to start position
|
|
925
|
+
await mouse.move(from.x, from.y);
|
|
926
|
+
// Press mouse button
|
|
927
|
+
await mouse.down();
|
|
928
|
+
// Calculate intermediate steps for smooth drag
|
|
929
|
+
const deltaX = (to.x - from.x) / steps;
|
|
930
|
+
const deltaY = (to.y - from.y) / steps;
|
|
931
|
+
try {
|
|
932
|
+
for (let i = 1; i <= steps; i++) {
|
|
933
|
+
// Abortable between steps: movement already dispatched
|
|
934
|
+
// stays dispatched, the rest of the drag does not go out.
|
|
935
|
+
throwIfAborted(abortSignal);
|
|
936
|
+
const currentX = from.x + deltaX * i;
|
|
937
|
+
const currentY = from.y + deltaY * i;
|
|
938
|
+
await mouse.move(currentX, currentY);
|
|
939
|
+
// Small delay for smoother drag
|
|
940
|
+
await abortableSleep(10, abortSignal);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
catch (err) {
|
|
944
|
+
// A cancel mid-drag must not leave the button held down.
|
|
945
|
+
if (isAbortError(err)) {
|
|
946
|
+
try {
|
|
947
|
+
await mouse.up();
|
|
948
|
+
}
|
|
949
|
+
catch { /* best-effort */ }
|
|
950
|
+
}
|
|
951
|
+
throw err;
|
|
952
|
+
}
|
|
953
|
+
// Release mouse button
|
|
954
|
+
await mouse.up();
|
|
955
|
+
return {
|
|
956
|
+
from,
|
|
957
|
+
to,
|
|
958
|
+
steps,
|
|
959
|
+
distance: Math.sqrt(Math.pow(to.x - from.x, 2) + Math.pow(to.y - from.y, 2)),
|
|
960
|
+
};
|
|
961
|
+
});
|
|
962
|
+
}, 'drag');
|
|
963
|
+
if (result.pausedAtBreakpoint) {
|
|
964
|
+
return createSuccessResponse('ACTION_PAUSED_AT_BREAKPOINT', {
|
|
965
|
+
action: 'drag',
|
|
966
|
+
...result.pauseInfo,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
const dragResult = result.result;
|
|
970
|
+
return {
|
|
971
|
+
content: [
|
|
972
|
+
{
|
|
973
|
+
type: 'text',
|
|
974
|
+
text: `Dragged from (${dragResult?.from.x}, ${dragResult?.from.y}) to (${dragResult?.to.x}, ${dragResult?.to.y})\n**Distance:** ${dragResult?.distance.toFixed(1)}px over ${dragResult?.steps} steps`,
|
|
975
|
+
},
|
|
976
|
+
],
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
case 'scroll': {
|
|
980
|
+
const { deltaX = 0, deltaY = 0, x, y } = args;
|
|
981
|
+
if (deltaX === 0 && deltaY === 0) {
|
|
982
|
+
return {
|
|
983
|
+
content: [
|
|
984
|
+
{
|
|
985
|
+
type: 'text',
|
|
986
|
+
text: `## Error\n\nAt least one of \`deltaX\` or \`deltaY\` must be non-zero\n\n**Action:** scroll\n\n**Suggestion:** Provide scroll amounts. Example: deltaY: 300 (scroll down), deltaY: -300 (scroll up)`,
|
|
987
|
+
},
|
|
988
|
+
],
|
|
989
|
+
isError: true,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
const result = await executeWithPauseDetection(targetCdpManager, async () => {
|
|
993
|
+
// Use wrapper to bypass replay blocker overlay for scroll operation
|
|
994
|
+
return await withReplayBypass(page, async () => {
|
|
995
|
+
const mouse = page.mouse;
|
|
996
|
+
// Abortable before anything is dispatched.
|
|
997
|
+
throwIfAborted(abortSignal);
|
|
998
|
+
// If coordinates provided, move to that position first
|
|
999
|
+
if (x !== undefined && y !== undefined) {
|
|
1000
|
+
await mouse.move(x, y);
|
|
1001
|
+
}
|
|
1002
|
+
// Perform scroll
|
|
1003
|
+
await mouse.wheel({ deltaX, deltaY });
|
|
1004
|
+
// Get current scroll position
|
|
1005
|
+
const scrollPosition = await page.evaluate(() => ({
|
|
1006
|
+
scrollX: globalThis.window.scrollX,
|
|
1007
|
+
scrollY: globalThis.window.scrollY,
|
|
1008
|
+
maxScrollX: globalThis.document.documentElement.scrollWidth - globalThis.window.innerWidth,
|
|
1009
|
+
maxScrollY: globalThis.document.documentElement.scrollHeight - globalThis.window.innerHeight,
|
|
1010
|
+
}));
|
|
1011
|
+
return {
|
|
1012
|
+
deltaX,
|
|
1013
|
+
deltaY,
|
|
1014
|
+
position: x !== undefined && y !== undefined ? { x, y } : undefined,
|
|
1015
|
+
scrollPosition,
|
|
1016
|
+
};
|
|
1017
|
+
});
|
|
1018
|
+
}, 'scroll');
|
|
1019
|
+
if (result.pausedAtBreakpoint) {
|
|
1020
|
+
return createSuccessResponse('ACTION_PAUSED_AT_BREAKPOINT', {
|
|
1021
|
+
action: 'scroll',
|
|
1022
|
+
...result.pauseInfo,
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
const scrollResult = result.result;
|
|
1026
|
+
const directionParts = [];
|
|
1027
|
+
if (deltaY > 0)
|
|
1028
|
+
directionParts.push(`down ${deltaY}px`);
|
|
1029
|
+
if (deltaY < 0)
|
|
1030
|
+
directionParts.push(`up ${Math.abs(deltaY)}px`);
|
|
1031
|
+
if (deltaX > 0)
|
|
1032
|
+
directionParts.push(`right ${deltaX}px`);
|
|
1033
|
+
if (deltaX < 0)
|
|
1034
|
+
directionParts.push(`left ${Math.abs(deltaX)}px`);
|
|
1035
|
+
const positionInfo = scrollResult?.position
|
|
1036
|
+
? ` at (${scrollResult.position.x}, ${scrollResult.position.y})`
|
|
1037
|
+
: '';
|
|
1038
|
+
return {
|
|
1039
|
+
content: [
|
|
1040
|
+
{
|
|
1041
|
+
type: 'text',
|
|
1042
|
+
text: `Scrolled ${directionParts.join(' and ')}${positionInfo}\n**Page position:** (${scrollResult?.scrollPosition.scrollX}, ${scrollResult?.scrollPosition.scrollY}) of (${scrollResult?.scrollPosition.maxScrollX}, ${scrollResult?.scrollPosition.maxScrollY})`,
|
|
1043
|
+
},
|
|
1044
|
+
],
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
case 'mousemove': {
|
|
1048
|
+
const { x, y } = args;
|
|
1049
|
+
if (x === undefined || y === undefined) {
|
|
1050
|
+
return {
|
|
1051
|
+
content: [
|
|
1052
|
+
{
|
|
1053
|
+
type: 'text',
|
|
1054
|
+
text: `## Error\n\nMissing required parameters: \`x\` and \`y\`\n\n**Action:** mousemove\n\n**Suggestion:** Provide coordinates. Example: x: 100, y: 200`,
|
|
1055
|
+
},
|
|
1056
|
+
],
|
|
1057
|
+
isError: true,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
const result = await executeWithPauseDetection(targetCdpManager, async () => {
|
|
1061
|
+
// Use wrapper to bypass replay blocker overlay for mousemove
|
|
1062
|
+
throwIfAborted(abortSignal); // last exit before dispatch
|
|
1063
|
+
await withReplayBypass(page, () => page.mouse.move(x, y));
|
|
1064
|
+
// Get element at the mouse position
|
|
1065
|
+
const elementInfo = await page.evaluate((mouseX, mouseY) => {
|
|
1066
|
+
const el = globalThis.document.elementFromPoint(mouseX, mouseY);
|
|
1067
|
+
if (!el)
|
|
1068
|
+
return null;
|
|
1069
|
+
const tag = el.tagName.toLowerCase();
|
|
1070
|
+
const text = el.textContent?.trim().substring(0, 30) || '';
|
|
1071
|
+
const id = el.id ? `#${el.id}` : '';
|
|
1072
|
+
const className = el.className && typeof el.className === 'string'
|
|
1073
|
+
? `.${el.className.trim().split(/\s+/).slice(0, 2).join('.')}`
|
|
1074
|
+
: '';
|
|
1075
|
+
return {
|
|
1076
|
+
tag,
|
|
1077
|
+
text: text || undefined,
|
|
1078
|
+
selector: id || className || tag,
|
|
1079
|
+
};
|
|
1080
|
+
}, x, y);
|
|
1081
|
+
return { x, y, elementInfo };
|
|
1082
|
+
}, 'mousemove');
|
|
1083
|
+
if (result.pausedAtBreakpoint) {
|
|
1084
|
+
return createSuccessResponse('ACTION_PAUSED_AT_BREAKPOINT', {
|
|
1085
|
+
action: 'mousemove',
|
|
1086
|
+
...result.pauseInfo,
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
const moveResult = result.result;
|
|
1090
|
+
const elementDesc = moveResult?.elementInfo
|
|
1091
|
+
? `\n**Element at position:** \`${moveResult.elementInfo.selector}\`${moveResult.elementInfo.text ? ` "${moveResult.elementInfo.text}"` : ''}`
|
|
1092
|
+
: '';
|
|
1093
|
+
return {
|
|
1094
|
+
content: [
|
|
1095
|
+
{
|
|
1096
|
+
type: 'text',
|
|
1097
|
+
text: `Mouse moved to (${moveResult?.x}, ${moveResult?.y})${elementDesc}`,
|
|
1098
|
+
},
|
|
1099
|
+
],
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
case 'tap':
|
|
1103
|
+
case 'swipe': {
|
|
1104
|
+
// Real touch events. Mouse actions do not produce touchstart/
|
|
1105
|
+
// touchmove, so a component that listens only for touch cannot be
|
|
1106
|
+
// driven by click or drag at all.
|
|
1107
|
+
const isSwipe = args.action === 'swipe';
|
|
1108
|
+
let start = isSwipe ? args.from : (typeof args.x === 'number' && typeof args.y === 'number' ? { x: args.x, y: args.y } : undefined);
|
|
1109
|
+
// A gesture across an element: its geometry is read now, because a
|
|
1110
|
+
// row's coordinates move with whatever is above it and a sequence
|
|
1111
|
+
// cannot know them in advance. Without this, gesture-revealed
|
|
1112
|
+
// actions get faked with a programmatic click in page JS, which
|
|
1113
|
+
// proves nothing about the gesture.
|
|
1114
|
+
let selectorEnd;
|
|
1115
|
+
if ((!isSwipe || args.direction) && !start && args.selector) {
|
|
1116
|
+
const rawSelector = args.selector;
|
|
1117
|
+
let selector = rawSelector;
|
|
1118
|
+
if (isExtendedSelector(selector)) {
|
|
1119
|
+
const resolved = await resolveSelector(page, selector);
|
|
1120
|
+
if ('error' in resolved) {
|
|
1121
|
+
return createErrorResponse('ELEMENT_NOT_FOUND', { selector: rawSelector, suggestion: resolved.suggestion });
|
|
1122
|
+
}
|
|
1123
|
+
selector = resolved.selector;
|
|
1124
|
+
}
|
|
1125
|
+
const box = await page.evaluate((sel) => {
|
|
1126
|
+
const el = globalThis.document.querySelector(sel);
|
|
1127
|
+
if (!el)
|
|
1128
|
+
return null;
|
|
1129
|
+
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
1130
|
+
const r = el.getBoundingClientRect();
|
|
1131
|
+
const doc = globalThis.document.documentElement;
|
|
1132
|
+
return {
|
|
1133
|
+
x: r.x + r.width / 2, y: r.y + r.height / 2,
|
|
1134
|
+
width: r.width, height: r.height,
|
|
1135
|
+
viewW: doc.clientWidth, viewH: doc.clientHeight,
|
|
1136
|
+
};
|
|
1137
|
+
}, selector);
|
|
1138
|
+
await cleanupResolvedSelector(page, selector);
|
|
1139
|
+
if (!box)
|
|
1140
|
+
return createErrorResponse('ELEMENT_NOT_FOUND', { selector: rawSelector });
|
|
1141
|
+
start = { x: box.x, y: box.y };
|
|
1142
|
+
if (args.direction) {
|
|
1143
|
+
const along = (args.direction === 'left' || args.direction === 'right') ? box.width : box.height;
|
|
1144
|
+
// Deliberately short. A swipe-action row has ranges - a nudge
|
|
1145
|
+
// peeks, further opens, further still commits the destructive
|
|
1146
|
+
// action - and 60% of a wide row lands in the last one. Travel
|
|
1147
|
+
// far enough to reveal, and let a caller that WANTS the
|
|
1148
|
+
// over-drag ask for it.
|
|
1149
|
+
const travel = args.distance ?? Math.min(Math.round(along * 0.6), 96);
|
|
1150
|
+
const dx = args.direction === 'left' ? -travel : args.direction === 'right' ? travel : 0;
|
|
1151
|
+
const dy = args.direction === 'up' ? -travel : args.direction === 'down' ? travel : 0;
|
|
1152
|
+
// Stay on-screen: a gesture ending outside the viewport lands
|
|
1153
|
+
// nowhere and reads as an unresponsive component.
|
|
1154
|
+
selectorEnd = {
|
|
1155
|
+
x: Math.max(1, Math.min(box.viewW - 1, box.x + dx)),
|
|
1156
|
+
y: Math.max(1, Math.min(box.viewH - 1, box.y + dy)),
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (!start) {
|
|
1161
|
+
return createErrorResponse('INVALID_PARAMETER', {
|
|
1162
|
+
parameter: isSwipe ? 'from' : 'selector/x,y',
|
|
1163
|
+
value: 'missing',
|
|
1164
|
+
message: isSwipe
|
|
1165
|
+
? 'swipe needs from:{x,y} and to:{x,y}, or selector + direction.'
|
|
1166
|
+
: 'tap needs either a selector or x and y.'
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
if (isSwipe && !args.to && !selectorEnd) {
|
|
1170
|
+
return createErrorResponse('INVALID_PARAMETER', {
|
|
1171
|
+
parameter: 'to',
|
|
1172
|
+
value: 'missing',
|
|
1173
|
+
message: 'swipe needs to:{x,y}, or direction alongside selector.'
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
const end = isSwipe ? (args.to ?? selectorEnd) : start;
|
|
1177
|
+
const steps = Math.max(1, args.steps ?? 10);
|
|
1178
|
+
const result = await executeWithPauseDetection(targetCdpManager, async () => {
|
|
1179
|
+
const client = await page.createCDPSession();
|
|
1180
|
+
const point = (x, y) => ({ x: Math.round(x), y: Math.round(y) });
|
|
1181
|
+
try {
|
|
1182
|
+
return await withReplayBypass(page, async () => {
|
|
1183
|
+
throwIfAborted(abortSignal);
|
|
1184
|
+
await client.send('Input.dispatchTouchEvent', {
|
|
1185
|
+
type: 'touchStart',
|
|
1186
|
+
touchPoints: [point(start.x, start.y)],
|
|
1187
|
+
});
|
|
1188
|
+
if (isSwipe) {
|
|
1189
|
+
// Paced by default. Unpaced, the moves arrive within a few
|
|
1190
|
+
// ms of each other - velocity of several px/ms, which
|
|
1191
|
+
// gesture handlers read as a flick. On a row whose flick
|
|
1192
|
+
// means "commit the destructive action", the safe-looking
|
|
1193
|
+
// call (no timing given, short travel) would fire it.
|
|
1194
|
+
// Distance does not protect against this; only pacing
|
|
1195
|
+
// does.
|
|
1196
|
+
const perStep = (args.durationMs ?? 300) / steps;
|
|
1197
|
+
for (let i = 1; i <= steps; i++) {
|
|
1198
|
+
throwIfAborted(abortSignal);
|
|
1199
|
+
await client.send('Input.dispatchTouchEvent', {
|
|
1200
|
+
type: 'touchMove',
|
|
1201
|
+
touchPoints: [point(start.x + ((end.x - start.x) * i) / steps, start.y + ((end.y - start.y) * i) / steps)],
|
|
1202
|
+
});
|
|
1203
|
+
if (perStep > 0 && i < steps)
|
|
1204
|
+
await abortableSleep(perStep, abortSignal);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
// touchEnd carries no points: the contact has lifted.
|
|
1208
|
+
await client.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
|
|
1209
|
+
return { from: point(start.x, start.y), to: point(end.x, end.y), steps: isSwipe ? steps : 0 };
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
finally {
|
|
1213
|
+
await client.detach();
|
|
1214
|
+
}
|
|
1215
|
+
}, args.action);
|
|
1216
|
+
const r = result.result;
|
|
1217
|
+
if (!r) {
|
|
1218
|
+
return createErrorResponse('INVALID_PARAMETER', {
|
|
1219
|
+
parameter: args.action,
|
|
1220
|
+
value: 'no result',
|
|
1221
|
+
message: `${args.action} dispatched but returned no result — the page may have navigated mid-gesture.`
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
return {
|
|
1225
|
+
content: [{
|
|
1226
|
+
type: 'text',
|
|
1227
|
+
text: isSwipe
|
|
1228
|
+
? `Swiped (touch) from (${r.from.x},${r.from.y}) to (${r.to.x},${r.to.y}) in ${r.steps} steps`
|
|
1229
|
+
: `Tapped (touch) at (${r.from.x},${r.from.y})`,
|
|
1230
|
+
}],
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
case 'pinch': {
|
|
1234
|
+
const { x, y, scaleFactor } = args;
|
|
1235
|
+
if (scaleFactor === undefined) {
|
|
1236
|
+
return {
|
|
1237
|
+
content: [
|
|
1238
|
+
{
|
|
1239
|
+
type: 'text',
|
|
1240
|
+
text: `## Error\n\nMissing required parameter: \`scaleFactor\`\n\n**Action:** pinch\n\n**Suggestion:** Provide a scale factor. Example: scaleFactor: 2.0 (zoom in 2x), scaleFactor: 0.5 (zoom out 50%)`,
|
|
1241
|
+
},
|
|
1242
|
+
],
|
|
1243
|
+
isError: true,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
const result = await executeWithPauseDetection(targetCdpManager, async () => {
|
|
1247
|
+
// Get viewport center if coordinates not provided
|
|
1248
|
+
const viewport = page.viewport();
|
|
1249
|
+
const centerX = x ?? (viewport?.width ?? 800) / 2;
|
|
1250
|
+
const centerY = y ?? (viewport?.height ?? 600) / 2;
|
|
1251
|
+
// Create CDP session for synthesizePinchGesture
|
|
1252
|
+
const client = await page.createCDPSession();
|
|
1253
|
+
try {
|
|
1254
|
+
// Use wrapper to bypass replay blocker overlay for pinch gesture
|
|
1255
|
+
return await withReplayBypass(page, async () => {
|
|
1256
|
+
// Last exit before the gesture goes on the wire (once
|
|
1257
|
+
// dispatched, the whole synthesized pinch runs in Chrome).
|
|
1258
|
+
throwIfAborted(abortSignal);
|
|
1259
|
+
await client.send('Input.synthesizePinchGesture', {
|
|
1260
|
+
x: centerX,
|
|
1261
|
+
y: centerY,
|
|
1262
|
+
scaleFactor,
|
|
1263
|
+
relativeSpeed: 300, // pixels per second
|
|
1264
|
+
gestureSourceType: 'touch',
|
|
1265
|
+
});
|
|
1266
|
+
return {
|
|
1267
|
+
x: centerX,
|
|
1268
|
+
y: centerY,
|
|
1269
|
+
scaleFactor,
|
|
1270
|
+
action: scaleFactor > 1 ? 'zoom in' : 'zoom out',
|
|
1271
|
+
};
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
finally {
|
|
1275
|
+
await client.detach();
|
|
1276
|
+
}
|
|
1277
|
+
}, 'pinch');
|
|
1278
|
+
if (result.pausedAtBreakpoint) {
|
|
1279
|
+
return createSuccessResponse('ACTION_PAUSED_AT_BREAKPOINT', {
|
|
1280
|
+
action: 'pinch',
|
|
1281
|
+
...result.pauseInfo,
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
const pinchResult = result.result;
|
|
1285
|
+
return {
|
|
1286
|
+
content: [
|
|
1287
|
+
{
|
|
1288
|
+
type: 'text',
|
|
1289
|
+
text: `Pinch ${pinchResult?.action} at (${pinchResult?.x}, ${pinchResult?.y}) with scale factor ${pinchResult?.scaleFactor}`,
|
|
1290
|
+
},
|
|
1291
|
+
],
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
default:
|
|
1295
|
+
return createErrorResponse('INVALID_ACTION', {
|
|
1296
|
+
action,
|
|
1297
|
+
validActions: 'click, type, press, hover, focus, focusNext, focusPrevious, drag, scroll, mousemove, pinch'
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
}),
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Helper function to get information about the currently focused element
|
|
1305
|
+
*/
|
|
1306
|
+
async function getFocusedElementInfo(page) {
|
|
1307
|
+
return await page.evaluate(() => {
|
|
1308
|
+
const focused = globalThis.document.activeElement;
|
|
1309
|
+
if (!focused || focused === globalThis.document.body) {
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
const tag = focused.tagName.toLowerCase();
|
|
1313
|
+
const type = focused.getAttribute('type') || undefined;
|
|
1314
|
+
const text = focused.textContent?.trim().substring(0, 50) ||
|
|
1315
|
+
focused.getAttribute('aria-label') ||
|
|
1316
|
+
focused.getAttribute('placeholder') ||
|
|
1317
|
+
focused.getAttribute('name') ||
|
|
1318
|
+
focused.getAttribute('id') ||
|
|
1319
|
+
'';
|
|
1320
|
+
// Build a description
|
|
1321
|
+
let description = tag;
|
|
1322
|
+
if (type)
|
|
1323
|
+
description += `[type="${type}"]`;
|
|
1324
|
+
if (text)
|
|
1325
|
+
description += ` "${text}"`;
|
|
1326
|
+
// Try to build a useful selector
|
|
1327
|
+
let selector;
|
|
1328
|
+
if (focused.id) {
|
|
1329
|
+
selector = `#${focused.id}`;
|
|
1330
|
+
}
|
|
1331
|
+
else if (focused.name) {
|
|
1332
|
+
selector = `${tag}[name="${focused.name}"]`;
|
|
1333
|
+
}
|
|
1334
|
+
else if (focused.className && typeof focused.className === 'string') {
|
|
1335
|
+
const classes = focused.className.trim().split(/\s+/).slice(0, 2).join('.');
|
|
1336
|
+
if (classes)
|
|
1337
|
+
selector = `${tag}.${classes}`;
|
|
1338
|
+
}
|
|
1339
|
+
// Helper to format element for display
|
|
1340
|
+
const formatEl = (el) => {
|
|
1341
|
+
const elTag = el.tagName.toLowerCase();
|
|
1342
|
+
const elText = el.textContent?.trim().substring(0, 20) ||
|
|
1343
|
+
el.getAttribute('aria-label') ||
|
|
1344
|
+
el.getAttribute('placeholder') ||
|
|
1345
|
+
'';
|
|
1346
|
+
const elType = el.getAttribute('type');
|
|
1347
|
+
if (elTag === 'input') {
|
|
1348
|
+
return `${elTag}[${elType || 'text'}]${elText ? ` "${elText}"` : ''}`;
|
|
1349
|
+
}
|
|
1350
|
+
return `${elTag}${elText ? ` "${elText}"` : ''}`;
|
|
1351
|
+
};
|
|
1352
|
+
// Get tabbable elements
|
|
1353
|
+
const tabbable = Array.from(globalThis.document.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((el) => {
|
|
1354
|
+
const style = globalThis.window.getComputedStyle(el);
|
|
1355
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
1356
|
+
});
|
|
1357
|
+
const currentIndex = tabbable.indexOf(focused);
|
|
1358
|
+
let prevTabbable = [];
|
|
1359
|
+
let nextTabbable = [];
|
|
1360
|
+
if (currentIndex !== -1) {
|
|
1361
|
+
// Previous 3 tabbable elements
|
|
1362
|
+
prevTabbable = tabbable.slice(Math.max(0, currentIndex - 3), currentIndex).map(formatEl);
|
|
1363
|
+
// Next 3 tabbable elements
|
|
1364
|
+
nextTabbable = tabbable.slice(currentIndex + 1, currentIndex + 4).map(formatEl);
|
|
1365
|
+
}
|
|
1366
|
+
return {
|
|
1367
|
+
description,
|
|
1368
|
+
tag,
|
|
1369
|
+
type,
|
|
1370
|
+
text: text || undefined,
|
|
1371
|
+
selector,
|
|
1372
|
+
prevTabbable,
|
|
1373
|
+
nextTabbable,
|
|
1374
|
+
};
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Helper function to dismiss a modal
|
|
1379
|
+
*
|
|
1380
|
+
* This is a thin wrapper around the shared dismissal logic from modal-dismissal.ts
|
|
1381
|
+
*/
|
|
1382
|
+
async function dismissModalHelper(page, modalSelector, strategy) {
|
|
1383
|
+
// Get modal info to determine strategy
|
|
1384
|
+
const modals = await detectModals(page);
|
|
1385
|
+
const modal = modals.find(m => m.selector === modalSelector);
|
|
1386
|
+
if (!modal) {
|
|
1387
|
+
return { success: false, error: 'Modal not found' };
|
|
1388
|
+
}
|
|
1389
|
+
// Use shared dismissal logic
|
|
1390
|
+
const effectiveStrategy = selectDismissalStrategy(modal, strategy);
|
|
1391
|
+
const result = await dismissModalByStrategy(page, modal, effectiveStrategy, 3);
|
|
1392
|
+
return {
|
|
1393
|
+
success: result.success,
|
|
1394
|
+
error: result.error,
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
//# sourceMappingURL=input-tools.js.map
|