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,2007 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction Recorder
|
|
3
|
+
*
|
|
4
|
+
* Records user interactions (mouse, keyboard, navigation) from a browser page
|
|
5
|
+
* for later replay or conversion to test scripts.
|
|
6
|
+
*
|
|
7
|
+
* Actions:
|
|
8
|
+
* - recordInteraction: Start recording
|
|
9
|
+
* - stopInteraction: Stop recording, keep in memory
|
|
10
|
+
* - replayInteraction: Get recorded events by index
|
|
11
|
+
* - clearInteraction: Clear recording from memory
|
|
12
|
+
*/
|
|
13
|
+
import { debugLog } from './debug-logger.js';
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// State
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Active recording session (one per connection)
|
|
18
|
+
const activeSessions = new Map();
|
|
19
|
+
let nextRecordingId = 1;
|
|
20
|
+
// Cleanup handles for page listeners
|
|
21
|
+
const cleanupHandles = new Map();
|
|
22
|
+
// Cancel callbacks for active recordings
|
|
23
|
+
const cancelCallbacks = new Map();
|
|
24
|
+
/**
|
|
25
|
+
* Cancel an active recording without saving
|
|
26
|
+
*/
|
|
27
|
+
export async function cancelRecording(connectionReference) {
|
|
28
|
+
const cancel = cancelCallbacks.get(connectionReference);
|
|
29
|
+
if (cancel) {
|
|
30
|
+
await cancel();
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Cancel all active recordings
|
|
37
|
+
*/
|
|
38
|
+
export async function cancelAllRecordings() {
|
|
39
|
+
const count = cancelCallbacks.size;
|
|
40
|
+
for (const cancel of cancelCallbacks.values()) {
|
|
41
|
+
await cancel();
|
|
42
|
+
}
|
|
43
|
+
return count;
|
|
44
|
+
}
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Type Guards
|
|
47
|
+
// =============================================================================
|
|
48
|
+
export function isMouseEvent(event) {
|
|
49
|
+
return 'x' in event && 'y' in event;
|
|
50
|
+
}
|
|
51
|
+
export function isKeyboardEvent(event) {
|
|
52
|
+
return 'key' in event && 'code' in event;
|
|
53
|
+
}
|
|
54
|
+
export function isNavigationEvent(event) {
|
|
55
|
+
return event.type === 'navigation' || event.type === 'reload';
|
|
56
|
+
}
|
|
57
|
+
export function isCommentEvent(event) {
|
|
58
|
+
return event.type === 'comment';
|
|
59
|
+
}
|
|
60
|
+
export function isPasteEvent(event) {
|
|
61
|
+
return event.type === 'paste';
|
|
62
|
+
}
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// Recording Functions
|
|
65
|
+
// =============================================================================
|
|
66
|
+
/**
|
|
67
|
+
* Start recording interactions on a page.
|
|
68
|
+
* Returns a promise that resolves when the recording is completed (via UI or stopRecording).
|
|
69
|
+
*/
|
|
70
|
+
export async function startRecording(page, connectionReference, options = {}) {
|
|
71
|
+
// If there's an orphaned recording (e.g., MCP didn't send abort signal), clean it up
|
|
72
|
+
if (activeSessions.has(connectionReference)) {
|
|
73
|
+
await cancelRecording(connectionReference);
|
|
74
|
+
}
|
|
75
|
+
const id = nextRecordingId++;
|
|
76
|
+
const startUrl = page.url();
|
|
77
|
+
const session = {
|
|
78
|
+
id,
|
|
79
|
+
connectionReference,
|
|
80
|
+
startTime: Date.now(),
|
|
81
|
+
startUrl,
|
|
82
|
+
events: [],
|
|
83
|
+
isRecording: true,
|
|
84
|
+
isPaused: false,
|
|
85
|
+
};
|
|
86
|
+
activeSessions.set(connectionReference, session);
|
|
87
|
+
// Note: When issueId is provided, the caller (workOn handler) has already shown
|
|
88
|
+
// the options overlay where the user chose to record. No need for another overlay here.
|
|
89
|
+
const showOverlayOption = options.showOverlay ?? true;
|
|
90
|
+
// Create a promise that will resolve when recording completes or is cancelled
|
|
91
|
+
let resolveRecording;
|
|
92
|
+
const recordingPromise = new Promise((resolve) => {
|
|
93
|
+
resolveRecording = resolve;
|
|
94
|
+
});
|
|
95
|
+
// Store cancel callback for external cancellation
|
|
96
|
+
const cancelFn = async () => {
|
|
97
|
+
// Clean up without creating a recording
|
|
98
|
+
const cleanup = cleanupHandles.get(connectionReference);
|
|
99
|
+
if (cleanup) {
|
|
100
|
+
await cleanup().catch(() => { });
|
|
101
|
+
cleanupHandles.delete(connectionReference);
|
|
102
|
+
}
|
|
103
|
+
activeSessions.delete(connectionReference);
|
|
104
|
+
cancelCallbacks.delete(connectionReference);
|
|
105
|
+
resolveRecording({ success: false, cancelled: true });
|
|
106
|
+
};
|
|
107
|
+
cancelCallbacks.set(connectionReference, cancelFn);
|
|
108
|
+
// Listen for abort signal from MCP (tool cancellation)
|
|
109
|
+
if (options.abortSignal) {
|
|
110
|
+
options.abortSignal.addEventListener('abort', () => {
|
|
111
|
+
cancelFn();
|
|
112
|
+
}, { once: true });
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
// Set up navigation tracking via CDP
|
|
116
|
+
const client = await page.createCDPSession();
|
|
117
|
+
let currentUrl = startUrl;
|
|
118
|
+
// Listen for navigation events
|
|
119
|
+
client.on('Page.frameNavigated', (params) => {
|
|
120
|
+
if (params.frame.parentId)
|
|
121
|
+
return; // Only main frame
|
|
122
|
+
const newUrl = params.frame.url;
|
|
123
|
+
if (newUrl !== currentUrl) {
|
|
124
|
+
const navEvent = {
|
|
125
|
+
type: 'navigation',
|
|
126
|
+
url: newUrl,
|
|
127
|
+
previousUrl: currentUrl,
|
|
128
|
+
timestamp: Date.now(),
|
|
129
|
+
};
|
|
130
|
+
session.events.push(navEvent);
|
|
131
|
+
currentUrl = newUrl;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
await client.send('Page.enable');
|
|
135
|
+
// Set up bindings for UI buttons to call back to server
|
|
136
|
+
await client.send('Runtime.addBinding', { name: '__cdpRecordingComplete' });
|
|
137
|
+
await client.send('Runtime.addBinding', { name: '__cdpRecordingCancel' });
|
|
138
|
+
// Binding to save events before navigation (so they're not lost on refresh)
|
|
139
|
+
await client.send('Runtime.addBinding', { name: '__cdpRecordingSaveEvents' });
|
|
140
|
+
client.on('Runtime.bindingCalled', async (event) => {
|
|
141
|
+
debugLog('recording', `Binding called: ${event.name}`);
|
|
142
|
+
if (event.name === '__cdpRecordingComplete') {
|
|
143
|
+
// UI stop button was clicked - process the recording
|
|
144
|
+
debugLog('recording', 'Processing recording completion...');
|
|
145
|
+
try {
|
|
146
|
+
const payload = JSON.parse(event.payload);
|
|
147
|
+
const browserEvents = payload.events;
|
|
148
|
+
const pausePeriods = payload.pausePeriods || [];
|
|
149
|
+
// Adjust event timestamps to exclude time spent in comment modals
|
|
150
|
+
// For each event, subtract the total pause time that occurred before it
|
|
151
|
+
const adjustedBrowserEvents = browserEvents.map(e => {
|
|
152
|
+
let adjustment = 0;
|
|
153
|
+
for (const pause of pausePeriods) {
|
|
154
|
+
if (e.timestamp > pause.end) {
|
|
155
|
+
// Event occurred after this pause ended - subtract full pause duration
|
|
156
|
+
adjustment += pause.end - pause.start;
|
|
157
|
+
}
|
|
158
|
+
else if (e.timestamp > pause.start) {
|
|
159
|
+
// Event occurred during pause (shouldn't happen normally, but handle it)
|
|
160
|
+
adjustment += e.timestamp - pause.start;
|
|
161
|
+
}
|
|
162
|
+
// If event occurred before pause started, no adjustment needed
|
|
163
|
+
}
|
|
164
|
+
return adjustment > 0 ? { ...e, timestamp: e.timestamp - adjustment } : e;
|
|
165
|
+
});
|
|
166
|
+
// Merge browser events with navigation events from session
|
|
167
|
+
const allEvents = [...session.events, ...adjustedBrowserEvents].sort((a, b) => a.timestamp - b.timestamp);
|
|
168
|
+
const endTime = Date.now();
|
|
169
|
+
const duration = endTime - session.startTime;
|
|
170
|
+
// Calculate summary
|
|
171
|
+
const summary = calculateSummary(allEvents);
|
|
172
|
+
// Store the recording
|
|
173
|
+
const stored = {
|
|
174
|
+
id: session.id,
|
|
175
|
+
connectionReference,
|
|
176
|
+
startTime: session.startTime,
|
|
177
|
+
endTime,
|
|
178
|
+
startUrl: session.startUrl,
|
|
179
|
+
duration,
|
|
180
|
+
events: allEvents,
|
|
181
|
+
summary,
|
|
182
|
+
};
|
|
183
|
+
// Cleanup
|
|
184
|
+
cleanupHandles.delete(connectionReference);
|
|
185
|
+
activeSessions.delete(connectionReference);
|
|
186
|
+
// Resolve the blocking promise - caller should close the tab
|
|
187
|
+
debugLog('recording', `Recording complete with ${allEvents.length} events`);
|
|
188
|
+
resolveRecording({ success: true, id: session.id, recording: stored, closeTab: true });
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
debugLog('recording', `Error processing UI stop: ${e}`);
|
|
192
|
+
resolveRecording({ success: false, error: String(e) });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (event.name === '__cdpRecordingCancel') {
|
|
196
|
+
// Cancel button was clicked - clean up without saving
|
|
197
|
+
cleanupHandles.delete(connectionReference);
|
|
198
|
+
activeSessions.delete(connectionReference);
|
|
199
|
+
cancelCallbacks.delete(connectionReference);
|
|
200
|
+
// Resolve with closeTab flag - caller should close the tab
|
|
201
|
+
resolveRecording({ success: false, cancelled: true, closeTab: true });
|
|
202
|
+
}
|
|
203
|
+
if (event.name === '__cdpRecordingSaveEvents') {
|
|
204
|
+
// Browser is about to navigate - save events to server-side session
|
|
205
|
+
try {
|
|
206
|
+
const payload = JSON.parse(event.payload);
|
|
207
|
+
const browserEvents = payload.events;
|
|
208
|
+
const pausePeriods = payload.pausePeriods || [];
|
|
209
|
+
// Adjust timestamps for pause periods
|
|
210
|
+
const adjustedEvents = browserEvents.map(e => {
|
|
211
|
+
let adjustment = 0;
|
|
212
|
+
for (const pause of pausePeriods) {
|
|
213
|
+
if (e.timestamp > pause.end) {
|
|
214
|
+
adjustment += pause.end - pause.start;
|
|
215
|
+
}
|
|
216
|
+
else if (e.timestamp > pause.start) {
|
|
217
|
+
adjustment += e.timestamp - pause.start;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return adjustment > 0 ? { ...e, timestamp: e.timestamp - adjustment } : e;
|
|
221
|
+
});
|
|
222
|
+
// Merge with existing session events (navigation events)
|
|
223
|
+
session.events = [...session.events, ...adjustedEvents].sort((a, b) => a.timestamp - b.timestamp);
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
console.error('[cdp-tools] Error saving events before navigation:', e);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
// Function to inject recording script - can be called multiple times (after navigation)
|
|
231
|
+
const injectRecordingScript = async () => {
|
|
232
|
+
await page.evaluate((showOverlayParam) => {
|
|
233
|
+
// Check if already injected (avoid duplicate listeners)
|
|
234
|
+
if (globalThis.__cdpRecordingInjected)
|
|
235
|
+
return;
|
|
236
|
+
globalThis.__cdpRecordingInjected = true;
|
|
237
|
+
globalThis.__cdpRecordingEvents = [];
|
|
238
|
+
globalThis.__cdpRecordingStart = Date.now();
|
|
239
|
+
globalThis.__cdpRecordingPaused = false;
|
|
240
|
+
globalThis.__cdpRecordingState = 'recording';
|
|
241
|
+
globalThis.__cdpPausePeriods = [];
|
|
242
|
+
const doc = globalThis.document;
|
|
243
|
+
// Save events to server before page unload (refresh/navigation)
|
|
244
|
+
// Use a flag to prevent double-saving (both beforeunload and pagehide can fire)
|
|
245
|
+
let eventsSaved = false;
|
|
246
|
+
const saveEventsBeforeUnload = () => {
|
|
247
|
+
if (eventsSaved)
|
|
248
|
+
return;
|
|
249
|
+
eventsSaved = true;
|
|
250
|
+
const events = globalThis.__cdpRecordingEvents || [];
|
|
251
|
+
const pausePeriods = globalThis.__cdpPausePeriods || [];
|
|
252
|
+
if (events.length > 0) {
|
|
253
|
+
try {
|
|
254
|
+
globalThis.__cdpRecordingSaveEvents(JSON.stringify({ events, pausePeriods }));
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
// Binding may not be available
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
globalThis.addEventListener('beforeunload', saveEventsBeforeUnload);
|
|
262
|
+
// Also handle pagehide for bfcache
|
|
263
|
+
globalThis.addEventListener('pagehide', saveEventsBeforeUnload);
|
|
264
|
+
// Build selector helper - returns unique selector or undefined
|
|
265
|
+
// Strategy aligned with element-collector.ts getUniqueSelector
|
|
266
|
+
const buildSelector = (el) => {
|
|
267
|
+
if (!el || el === doc.body || el === doc.documentElement)
|
|
268
|
+
return undefined;
|
|
269
|
+
const tag = el.tagName?.toLowerCase();
|
|
270
|
+
// 1. ID is always unique and most specific
|
|
271
|
+
if (el.id)
|
|
272
|
+
return `#${el.id}`;
|
|
273
|
+
// 2. For inputs, use name attribute (stable form identifier)
|
|
274
|
+
if (el.name && (tag === 'input' || tag === 'textarea' || tag === 'select')) {
|
|
275
|
+
const selector = `${tag}[name="${el.name}"]`;
|
|
276
|
+
try {
|
|
277
|
+
if (doc.querySelectorAll(selector).length === 1)
|
|
278
|
+
return selector;
|
|
279
|
+
}
|
|
280
|
+
catch (e) { /* invalid selector */ }
|
|
281
|
+
}
|
|
282
|
+
// 3. aria-label is stable and descriptive
|
|
283
|
+
const ariaLabel = el.getAttribute?.('aria-label');
|
|
284
|
+
if (ariaLabel && ariaLabel.length <= 40) {
|
|
285
|
+
const selector = `${tag}[aria-label="${ariaLabel}"]`;
|
|
286
|
+
try {
|
|
287
|
+
if (doc.querySelectorAll(selector).length === 1)
|
|
288
|
+
return selector;
|
|
289
|
+
}
|
|
290
|
+
catch (e) { /* invalid selector */ }
|
|
291
|
+
}
|
|
292
|
+
// 4. href for links (prefer short/relative hrefs)
|
|
293
|
+
if (tag === 'a' && el.href) {
|
|
294
|
+
const href = el.getAttribute?.('href');
|
|
295
|
+
if (href && href !== '#' && !href.startsWith('javascript:') && href.length <= 60) {
|
|
296
|
+
const selector = `a[href="${href}"]`;
|
|
297
|
+
try {
|
|
298
|
+
if (doc.querySelectorAll(selector).length === 1)
|
|
299
|
+
return selector;
|
|
300
|
+
}
|
|
301
|
+
catch (e) { /* invalid selector */ }
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// 5. Try data-testid and other test attributes
|
|
305
|
+
const dataAttrs = ['data-testid', 'data-test-id', 'data-cy', 'data-id'];
|
|
306
|
+
for (const attr of dataAttrs) {
|
|
307
|
+
const val = el.getAttribute?.(attr);
|
|
308
|
+
if (val) {
|
|
309
|
+
const selector = `${tag}[${attr}="${val}"]`;
|
|
310
|
+
try {
|
|
311
|
+
if (doc.querySelectorAll(selector).length === 1)
|
|
312
|
+
return selector;
|
|
313
|
+
}
|
|
314
|
+
catch (e) { /* invalid selector */ }
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// 6. Text-based selector using :has-text() (cdp-tools extended selector)
|
|
318
|
+
const text = el.textContent?.trim();
|
|
319
|
+
if (text && text.length > 0 && text.length <= 30) {
|
|
320
|
+
const escapedText = text.replace(/"/g, '\\"');
|
|
321
|
+
// :has-text is supported by cdp-tools selector resolver
|
|
322
|
+
return `${tag}:has-text("${escapedText}")`;
|
|
323
|
+
}
|
|
324
|
+
// 7. Try class-based selector (filter out hash/generated classes)
|
|
325
|
+
if (el.className && typeof el.className === 'string') {
|
|
326
|
+
const classes = el.className.split(' ').filter((c) => c.length > 0 && c.length <= 20 && !c.includes('__') && !c.match(/^[a-z]+-[a-f0-9]+$/i));
|
|
327
|
+
if (classes.length > 0) {
|
|
328
|
+
const selector = `${tag}.${classes.slice(0, 2).join('.')}`;
|
|
329
|
+
try {
|
|
330
|
+
if (doc.querySelectorAll(selector).length === 1)
|
|
331
|
+
return selector;
|
|
332
|
+
}
|
|
333
|
+
catch (e) { /* invalid selector */ }
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// 8. No unique selector found - return undefined to fall back to coordinates
|
|
337
|
+
return undefined;
|
|
338
|
+
};
|
|
339
|
+
// Get element info helper
|
|
340
|
+
const getElementInfo = (el) => {
|
|
341
|
+
if (!el)
|
|
342
|
+
return undefined;
|
|
343
|
+
const tag = el.tagName?.toLowerCase();
|
|
344
|
+
const rect = el.getBoundingClientRect?.();
|
|
345
|
+
const isCanvas = tag === 'canvas' || tag === 'svg' || el.closest?.('canvas, svg');
|
|
346
|
+
const isInteractive = ['a', 'button', 'input', 'select', 'textarea'].includes(tag) ||
|
|
347
|
+
el.hasAttribute?.('onclick') ||
|
|
348
|
+
(el.hasAttribute?.('role') && ['button', 'link', 'checkbox', 'radio'].includes(el.getAttribute('role')));
|
|
349
|
+
return {
|
|
350
|
+
tag,
|
|
351
|
+
id: el.id || undefined,
|
|
352
|
+
className: typeof el.className === 'string' ? el.className.trim() : undefined,
|
|
353
|
+
text: el.textContent?.trim().substring(0, 50) || el.getAttribute?.('aria-label') || undefined,
|
|
354
|
+
selector: buildSelector(el),
|
|
355
|
+
isCanvas: isCanvas || undefined,
|
|
356
|
+
isInteractive: isInteractive || undefined,
|
|
357
|
+
boundingBox: rect ? { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) } : undefined,
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
// Check if element is part of our overlay UI
|
|
361
|
+
const isOurUI = (el) => {
|
|
362
|
+
if (!el)
|
|
363
|
+
return false;
|
|
364
|
+
// Check if element or any parent is our overlay
|
|
365
|
+
let current = el;
|
|
366
|
+
while (current) {
|
|
367
|
+
if (current.id === '__cdp-recording-overlay')
|
|
368
|
+
return true;
|
|
369
|
+
if (current.classList?.contains('cdp-panel'))
|
|
370
|
+
return true;
|
|
371
|
+
if (current.classList?.contains('cdp-btn'))
|
|
372
|
+
return true;
|
|
373
|
+
if (current.classList?.contains('cdp-comment-modal'))
|
|
374
|
+
return true;
|
|
375
|
+
current = current.parentElement;
|
|
376
|
+
}
|
|
377
|
+
return false;
|
|
378
|
+
};
|
|
379
|
+
// Capture mouse event
|
|
380
|
+
const captureMouseEvent = (e, type) => {
|
|
381
|
+
if (globalThis.__cdpRecordingPaused)
|
|
382
|
+
return;
|
|
383
|
+
if (globalThis.__cdpCommentModalOpen)
|
|
384
|
+
return;
|
|
385
|
+
if (isOurUI(e.target))
|
|
386
|
+
return;
|
|
387
|
+
const event = {
|
|
388
|
+
type,
|
|
389
|
+
x: e.clientX,
|
|
390
|
+
y: e.clientY,
|
|
391
|
+
timestamp: Date.now(),
|
|
392
|
+
button: e.button,
|
|
393
|
+
buttons: e.buttons,
|
|
394
|
+
};
|
|
395
|
+
if (type === 'wheel') {
|
|
396
|
+
event.deltaX = e.deltaX;
|
|
397
|
+
event.deltaY = e.deltaY;
|
|
398
|
+
}
|
|
399
|
+
if (type === 'click' || type === 'dblclick') {
|
|
400
|
+
event.clickCount = type === 'dblclick' ? 2 : 1;
|
|
401
|
+
}
|
|
402
|
+
event.elementInfo = getElementInfo(e.target);
|
|
403
|
+
globalThis.__cdpRecordingEvents.push(event);
|
|
404
|
+
updateOverlay(event, type);
|
|
405
|
+
};
|
|
406
|
+
// Capture keyboard event
|
|
407
|
+
const captureKeyEvent = (e, type) => {
|
|
408
|
+
if (globalThis.__cdpRecordingPaused)
|
|
409
|
+
return;
|
|
410
|
+
if (globalThis.__cdpCommentModalOpen)
|
|
411
|
+
return;
|
|
412
|
+
// Skip modifier-only keys (they'll be captured with the actual key press)
|
|
413
|
+
if (['Shift', 'Control', 'Alt', 'Meta'].includes(e.key))
|
|
414
|
+
return;
|
|
415
|
+
// Skip the comment shortcut (Ctrl/Cmd + Shift + C)
|
|
416
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'c')
|
|
417
|
+
return;
|
|
418
|
+
const target = e.target;
|
|
419
|
+
const event = {
|
|
420
|
+
type,
|
|
421
|
+
key: e.key,
|
|
422
|
+
code: e.code,
|
|
423
|
+
timestamp: Date.now(),
|
|
424
|
+
repeat: e.repeat || undefined,
|
|
425
|
+
};
|
|
426
|
+
if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) {
|
|
427
|
+
event.modifiers = {
|
|
428
|
+
ctrl: e.ctrlKey || undefined,
|
|
429
|
+
alt: e.altKey || undefined,
|
|
430
|
+
shift: e.shiftKey || undefined,
|
|
431
|
+
meta: e.metaKey || undefined,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const tag = target?.tagName?.toLowerCase();
|
|
435
|
+
if (target && tag) {
|
|
436
|
+
event.targetInfo = {
|
|
437
|
+
tag,
|
|
438
|
+
id: target.id || undefined,
|
|
439
|
+
isInput: ['input', 'textarea', 'select'].includes(tag) || target.isContentEditable,
|
|
440
|
+
selector: buildSelector(target),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
globalThis.__cdpRecordingEvents.push(event);
|
|
444
|
+
updateOverlay(event, type);
|
|
445
|
+
};
|
|
446
|
+
// Capture paste event
|
|
447
|
+
const capturePasteEvent = (e) => {
|
|
448
|
+
if (globalThis.__cdpRecordingPaused)
|
|
449
|
+
return;
|
|
450
|
+
if (globalThis.__cdpCommentModalOpen)
|
|
451
|
+
return;
|
|
452
|
+
if (isOurUI(e.target))
|
|
453
|
+
return;
|
|
454
|
+
let pastedText = e.clipboardData?.getData('text');
|
|
455
|
+
if (!pastedText)
|
|
456
|
+
return; // Only capture text paste events
|
|
457
|
+
const target = e.target;
|
|
458
|
+
const tag = target?.tagName?.toLowerCase();
|
|
459
|
+
// Normalize text based on target element type
|
|
460
|
+
// Single-line inputs can't accept newlines - the browser strips them
|
|
461
|
+
// So we store what will actually end up in the field
|
|
462
|
+
if (tag === 'input') {
|
|
463
|
+
// Replace newlines with empty string for single-line inputs
|
|
464
|
+
pastedText = pastedText.replace(/[\r\n]/g, '');
|
|
465
|
+
}
|
|
466
|
+
const event = {
|
|
467
|
+
type: 'paste',
|
|
468
|
+
text: pastedText,
|
|
469
|
+
timestamp: Date.now(),
|
|
470
|
+
};
|
|
471
|
+
if (target && tag) {
|
|
472
|
+
event.targetInfo = {
|
|
473
|
+
tag,
|
|
474
|
+
id: target.id || undefined,
|
|
475
|
+
isInput: ['input', 'textarea', 'select'].includes(tag) || target.isContentEditable,
|
|
476
|
+
selector: buildSelector(target),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
// Remove the preceding Cmd/Ctrl+V keydown events since we're capturing the paste content directly
|
|
480
|
+
const events = globalThis.__cdpRecordingEvents;
|
|
481
|
+
// Look backwards for recent keydown events that are part of the paste shortcut
|
|
482
|
+
// (within last 500ms to account for any delay between keydown and paste event)
|
|
483
|
+
const now = Date.now();
|
|
484
|
+
while (events.length > 0) {
|
|
485
|
+
const lastEvent = events[events.length - 1];
|
|
486
|
+
const timeDiff = now - lastEvent.timestamp;
|
|
487
|
+
const isRecentKeydown = lastEvent.type === 'keydown' && timeDiff < 500;
|
|
488
|
+
const isPasteKey = (lastEvent.key === 'v' || lastEvent.key === 'V') &&
|
|
489
|
+
(lastEvent.modifiers?.meta || lastEvent.modifiers?.ctrl);
|
|
490
|
+
if (isRecentKeydown && isPasteKey) {
|
|
491
|
+
events.pop(); // Remove the 'v' keydown
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
events.push(event);
|
|
498
|
+
updateOverlay(event, 'paste');
|
|
499
|
+
};
|
|
500
|
+
// Overlay
|
|
501
|
+
let overlay = null;
|
|
502
|
+
const updateOverlay = (event, type) => {
|
|
503
|
+
if (!overlay || globalThis.__cdpRecordingState === 'completed')
|
|
504
|
+
return;
|
|
505
|
+
const events = globalThis.__cdpRecordingEvents;
|
|
506
|
+
const duration = ((Date.now() - globalThis.__cdpRecordingStart) / 1000).toFixed(1);
|
|
507
|
+
const isPaused = globalThis.__cdpRecordingPaused;
|
|
508
|
+
// Use stored references (set after DOM creation)
|
|
509
|
+
const statsEl = globalThis.__cdpStatsEl;
|
|
510
|
+
const eventEl = globalThis.__cdpEventEl;
|
|
511
|
+
const statusEl = globalThis.__cdpStatusEl;
|
|
512
|
+
if (statsEl)
|
|
513
|
+
statsEl.textContent = `${events.length} | ${duration}s`;
|
|
514
|
+
if (statusEl) {
|
|
515
|
+
statusEl.textContent = isPaused ? 'PAUSED' : 'REC';
|
|
516
|
+
statusEl.style.background = isPaused ? '#eab308' : '#ef4444';
|
|
517
|
+
}
|
|
518
|
+
if (eventEl) {
|
|
519
|
+
const isKey = type === 'keydown' || type === 'keyup';
|
|
520
|
+
const isComment = type === 'comment';
|
|
521
|
+
const isPaste = type === 'paste';
|
|
522
|
+
eventEl.textContent = isComment ? 'NOTE' : isPaste ? 'PASTE' : isKey ? event.key : type.replace('mouse', '').toUpperCase();
|
|
523
|
+
eventEl.style.background = isComment ? '#f59e0b' :
|
|
524
|
+
isPaste ? '#9333ea' :
|
|
525
|
+
type === 'click' ? '#ef4444' :
|
|
526
|
+
type === 'wheel' ? '#3b82f6' :
|
|
527
|
+
isKey ? '#10b981' :
|
|
528
|
+
type.includes('mouse') ? '#8b5cf6' : '#6b7280';
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
if (showOverlayParam) {
|
|
532
|
+
overlay = doc.createElement('div');
|
|
533
|
+
overlay.id = '__cdp-recording-overlay';
|
|
534
|
+
overlay.style.cssText = 'opacity: 1 !important;';
|
|
535
|
+
// Build overlay using DOM methods (Trusted Types compatible)
|
|
536
|
+
const panel = doc.createElement('div');
|
|
537
|
+
panel.className = 'cdp-panel';
|
|
538
|
+
panel.style.cssText = `
|
|
539
|
+
position: fixed !important;
|
|
540
|
+
bottom: 10px !important;
|
|
541
|
+
left: 50% !important;
|
|
542
|
+
transform: translateX(-50%) !important;
|
|
543
|
+
background: rgba(0, 0, 0, 0.9) !important;
|
|
544
|
+
color: white !important;
|
|
545
|
+
padding: 6px 12px !important;
|
|
546
|
+
border-radius: 20px !important;
|
|
547
|
+
font-family: -apple-system, system-ui, sans-serif !important;
|
|
548
|
+
font-size: 12px !important;
|
|
549
|
+
z-index: 2147483647 !important;
|
|
550
|
+
display: flex !important;
|
|
551
|
+
align-items: center !important;
|
|
552
|
+
gap: 10px !important;
|
|
553
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important;
|
|
554
|
+
backdrop-filter: blur(4px) !important;
|
|
555
|
+
opacity: 1 !important;
|
|
556
|
+
`;
|
|
557
|
+
const statusEl = doc.createElement('span');
|
|
558
|
+
statusEl.className = 'cdp-status';
|
|
559
|
+
statusEl.textContent = 'REC';
|
|
560
|
+
Object.assign(statusEl.style, {
|
|
561
|
+
padding: '2px 6px',
|
|
562
|
+
borderRadius: '4px',
|
|
563
|
+
fontSize: '10px',
|
|
564
|
+
fontWeight: '600',
|
|
565
|
+
background: '#ef4444',
|
|
566
|
+
letterSpacing: '0.5px'
|
|
567
|
+
});
|
|
568
|
+
const statsEl = doc.createElement('span');
|
|
569
|
+
statsEl.className = 'cdp-stats';
|
|
570
|
+
statsEl.textContent = '0 | 0.0s';
|
|
571
|
+
Object.assign(statsEl.style, { color: '#d1d5db', minWidth: '60px' });
|
|
572
|
+
const eventEl = doc.createElement('span');
|
|
573
|
+
eventEl.className = 'cdp-event';
|
|
574
|
+
eventEl.textContent = '-';
|
|
575
|
+
Object.assign(eventEl.style, {
|
|
576
|
+
padding: '2px 8px',
|
|
577
|
+
borderRadius: '4px',
|
|
578
|
+
fontSize: '10px',
|
|
579
|
+
background: '#6b7280',
|
|
580
|
+
minWidth: '50px',
|
|
581
|
+
textAlign: 'center'
|
|
582
|
+
});
|
|
583
|
+
const btnContainer = doc.createElement('div');
|
|
584
|
+
Object.assign(btnContainer.style, { display: 'flex', gap: '4px', marginLeft: '6px' });
|
|
585
|
+
const btnStyle = {
|
|
586
|
+
background: '#374151',
|
|
587
|
+
border: 'none',
|
|
588
|
+
color: 'white',
|
|
589
|
+
width: '24px',
|
|
590
|
+
height: '24px',
|
|
591
|
+
borderRadius: '4px',
|
|
592
|
+
cursor: 'pointer',
|
|
593
|
+
display: 'flex',
|
|
594
|
+
alignItems: 'center',
|
|
595
|
+
justifyContent: 'center',
|
|
596
|
+
fontSize: '10px'
|
|
597
|
+
};
|
|
598
|
+
const commentBtn = doc.createElement('button');
|
|
599
|
+
commentBtn.className = 'cdp-btn cdp-comment';
|
|
600
|
+
commentBtn.title = 'Add Comment (Cmd+Shift+C) | Bug (Cmd+Shift+B) | Feature (Cmd+Shift+F)';
|
|
601
|
+
commentBtn.textContent = '💬';
|
|
602
|
+
Object.assign(commentBtn.style, btnStyle);
|
|
603
|
+
const pauseBtn = doc.createElement('button');
|
|
604
|
+
pauseBtn.className = 'cdp-btn cdp-pause';
|
|
605
|
+
pauseBtn.title = 'Pause/Resume';
|
|
606
|
+
pauseBtn.textContent = '⏸';
|
|
607
|
+
Object.assign(pauseBtn.style, btnStyle);
|
|
608
|
+
const resetBtn = doc.createElement('button');
|
|
609
|
+
resetBtn.className = 'cdp-btn cdp-reset';
|
|
610
|
+
resetBtn.title = 'Reset';
|
|
611
|
+
resetBtn.textContent = '↺';
|
|
612
|
+
Object.assign(resetBtn.style, btnStyle);
|
|
613
|
+
const doneBtn = doc.createElement('button');
|
|
614
|
+
doneBtn.className = 'cdp-btn cdp-done';
|
|
615
|
+
doneBtn.title = 'Complete';
|
|
616
|
+
doneBtn.textContent = '✓';
|
|
617
|
+
Object.assign(doneBtn.style, { ...btnStyle, background: '#059669' });
|
|
618
|
+
const cancelBtn = doc.createElement('button');
|
|
619
|
+
cancelBtn.className = 'cdp-btn cdp-cancel';
|
|
620
|
+
cancelBtn.title = 'Cancel (discard recording)';
|
|
621
|
+
cancelBtn.textContent = '✕';
|
|
622
|
+
Object.assign(cancelBtn.style, { ...btnStyle, background: '#dc2626' });
|
|
623
|
+
btnContainer.appendChild(commentBtn);
|
|
624
|
+
btnContainer.appendChild(pauseBtn);
|
|
625
|
+
btnContainer.appendChild(resetBtn);
|
|
626
|
+
btnContainer.appendChild(doneBtn);
|
|
627
|
+
btnContainer.appendChild(cancelBtn);
|
|
628
|
+
panel.appendChild(statusEl);
|
|
629
|
+
panel.appendChild(statsEl);
|
|
630
|
+
panel.appendChild(eventEl);
|
|
631
|
+
panel.appendChild(btnContainer);
|
|
632
|
+
// Comment modal
|
|
633
|
+
const commentModal = doc.createElement('div');
|
|
634
|
+
commentModal.className = 'cdp-comment-modal';
|
|
635
|
+
commentModal.style.cssText = `
|
|
636
|
+
display: none !important;
|
|
637
|
+
position: fixed !important;
|
|
638
|
+
top: 0 !important;
|
|
639
|
+
left: 0 !important;
|
|
640
|
+
width: 100% !important;
|
|
641
|
+
height: 100% !important;
|
|
642
|
+
background: rgba(0, 0, 0, 0.9) !important;
|
|
643
|
+
z-index: 2147483648 !important;
|
|
644
|
+
justify-content: center !important;
|
|
645
|
+
align-items: center !important;
|
|
646
|
+
font-family: -apple-system, system-ui, sans-serif !important;
|
|
647
|
+
opacity: 1 !important;
|
|
648
|
+
`;
|
|
649
|
+
const modalCard = doc.createElement('div');
|
|
650
|
+
modalCard.style.cssText = `
|
|
651
|
+
background: #1f2937 !important;
|
|
652
|
+
border-radius: 12px !important;
|
|
653
|
+
padding: 24px !important;
|
|
654
|
+
width: 90% !important;
|
|
655
|
+
max-width: 500px !important;
|
|
656
|
+
box-shadow: 0 25px 50px rgba(0,0,0,0.5) !important;
|
|
657
|
+
opacity: 1 !important;
|
|
658
|
+
`;
|
|
659
|
+
const modalTitle = doc.createElement('h3');
|
|
660
|
+
modalTitle.textContent = 'Add Comment';
|
|
661
|
+
Object.assign(modalTitle.style, { margin: '0 0 16px 0', color: 'white', fontSize: '18px' });
|
|
662
|
+
// Category buttons
|
|
663
|
+
const categoryContainer = doc.createElement('div');
|
|
664
|
+
categoryContainer.className = 'cdp-comment-category';
|
|
665
|
+
Object.assign(categoryContainer.style, { display: 'flex', gap: '0', marginBottom: '16px' });
|
|
666
|
+
const categories = [
|
|
667
|
+
{ value: 'narrative', label: 'NARRATIVE', borderRadius: '6px 0 0 6px' },
|
|
668
|
+
{ value: 'bug', label: 'BUG', borderRadius: '0' },
|
|
669
|
+
{ value: 'feature', label: 'FEATURE', borderRadius: '0 6px 6px 0' }
|
|
670
|
+
];
|
|
671
|
+
const categoryRadios = [];
|
|
672
|
+
const categoryBtnsArr = [];
|
|
673
|
+
categories.forEach((cat, i) => {
|
|
674
|
+
const label = doc.createElement('label');
|
|
675
|
+
Object.assign(label.style, { flex: '1', cursor: 'pointer' });
|
|
676
|
+
const radio = doc.createElement('input');
|
|
677
|
+
radio.type = 'radio';
|
|
678
|
+
radio.name = 'cdp-category';
|
|
679
|
+
radio.value = cat.value;
|
|
680
|
+
if (i === 0)
|
|
681
|
+
radio.checked = true;
|
|
682
|
+
radio.style.display = 'none';
|
|
683
|
+
categoryRadios.push(radio);
|
|
684
|
+
const span = doc.createElement('span');
|
|
685
|
+
span.className = 'cdp-cat-btn';
|
|
686
|
+
span.dataset.cat = cat.value;
|
|
687
|
+
span.textContent = cat.label;
|
|
688
|
+
Object.assign(span.style, {
|
|
689
|
+
display: 'block',
|
|
690
|
+
textAlign: 'center',
|
|
691
|
+
padding: '8px 12px',
|
|
692
|
+
background: i === 0 ? '#3b82f6' : '#374151',
|
|
693
|
+
color: i === 0 ? 'white' : '#9ca3af',
|
|
694
|
+
fontSize: '13px',
|
|
695
|
+
fontWeight: '500',
|
|
696
|
+
borderRadius: cat.borderRadius,
|
|
697
|
+
border: i === 0 ? '1px solid #3b82f6' : '1px solid #4b5563'
|
|
698
|
+
});
|
|
699
|
+
categoryBtnsArr.push(span);
|
|
700
|
+
label.appendChild(radio);
|
|
701
|
+
label.appendChild(span);
|
|
702
|
+
categoryContainer.appendChild(label);
|
|
703
|
+
});
|
|
704
|
+
const commentInput = doc.createElement('textarea');
|
|
705
|
+
commentInput.className = 'cdp-comment-input';
|
|
706
|
+
commentInput.placeholder = "e.g., 'Expected the form to show a success message'";
|
|
707
|
+
Object.assign(commentInput.style, {
|
|
708
|
+
width: '100%',
|
|
709
|
+
height: '120px',
|
|
710
|
+
background: '#374151',
|
|
711
|
+
border: '1px solid #4b5563',
|
|
712
|
+
borderRadius: '8px',
|
|
713
|
+
color: 'white',
|
|
714
|
+
padding: '12px',
|
|
715
|
+
fontSize: '14px',
|
|
716
|
+
resize: 'vertical',
|
|
717
|
+
boxSizing: 'border-box'
|
|
718
|
+
});
|
|
719
|
+
const modalBtnContainer = doc.createElement('div');
|
|
720
|
+
Object.assign(modalBtnContainer.style, { display: 'flex', gap: '12px', marginTop: '16px', justifyContent: 'flex-end' });
|
|
721
|
+
const commentCancel = doc.createElement('button');
|
|
722
|
+
commentCancel.className = 'cdp-comment-cancel';
|
|
723
|
+
commentCancel.textContent = 'Cancel';
|
|
724
|
+
Object.assign(commentCancel.style, {
|
|
725
|
+
background: '#374151',
|
|
726
|
+
border: 'none',
|
|
727
|
+
color: 'white',
|
|
728
|
+
padding: '8px 16px',
|
|
729
|
+
borderRadius: '6px',
|
|
730
|
+
cursor: 'pointer',
|
|
731
|
+
fontSize: '14px'
|
|
732
|
+
});
|
|
733
|
+
const commentSave = doc.createElement('button');
|
|
734
|
+
commentSave.className = 'cdp-comment-save';
|
|
735
|
+
commentSave.textContent = 'Save Comment';
|
|
736
|
+
Object.assign(commentSave.style, {
|
|
737
|
+
background: '#3b82f6',
|
|
738
|
+
border: 'none',
|
|
739
|
+
color: 'white',
|
|
740
|
+
padding: '8px 16px',
|
|
741
|
+
borderRadius: '6px',
|
|
742
|
+
cursor: 'pointer',
|
|
743
|
+
fontSize: '14px'
|
|
744
|
+
});
|
|
745
|
+
modalBtnContainer.appendChild(commentCancel);
|
|
746
|
+
modalBtnContainer.appendChild(commentSave);
|
|
747
|
+
modalCard.appendChild(modalTitle);
|
|
748
|
+
modalCard.appendChild(categoryContainer);
|
|
749
|
+
modalCard.appendChild(commentInput);
|
|
750
|
+
modalCard.appendChild(modalBtnContainer);
|
|
751
|
+
commentModal.appendChild(modalCard);
|
|
752
|
+
overlay.appendChild(panel);
|
|
753
|
+
overlay.appendChild(commentModal);
|
|
754
|
+
doc.body.appendChild(overlay);
|
|
755
|
+
// Store references for updateOverlay
|
|
756
|
+
globalThis.__cdpStatusEl = statusEl;
|
|
757
|
+
globalThis.__cdpStatsEl = statsEl;
|
|
758
|
+
globalThis.__cdpEventEl = eventEl;
|
|
759
|
+
const categoryBtns = categoryBtnsArr;
|
|
760
|
+
// Category button styling
|
|
761
|
+
const updateCategoryStyles = () => {
|
|
762
|
+
categoryBtns.forEach((btn) => {
|
|
763
|
+
const cat = btn.dataset.cat;
|
|
764
|
+
const radio = overlay.querySelector(`input[value="${cat}"]`);
|
|
765
|
+
if (radio?.checked) {
|
|
766
|
+
btn.style.background = cat === 'bug' ? '#dc2626' : cat === 'feature' ? '#059669' : '#3b82f6';
|
|
767
|
+
btn.style.color = 'white';
|
|
768
|
+
btn.style.borderColor = btn.style.background;
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
btn.style.background = '#374151';
|
|
772
|
+
btn.style.color = '#9ca3af';
|
|
773
|
+
btn.style.borderColor = '#4b5563';
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
};
|
|
777
|
+
categoryRadios.forEach((radio) => {
|
|
778
|
+
radio.addEventListener('change', updateCategoryStyles);
|
|
779
|
+
});
|
|
780
|
+
// Comment modal functions
|
|
781
|
+
const showCommentModal = (category = 'narrative') => {
|
|
782
|
+
if (commentModal) {
|
|
783
|
+
globalThis.__cdpCommentModalOpen = true;
|
|
784
|
+
globalThis.__cdpCommentModalOpenedAt = Date.now();
|
|
785
|
+
commentModal.style.display = 'flex';
|
|
786
|
+
// Set the specified category
|
|
787
|
+
const radio = overlay.querySelector(`input[value="${category}"]`);
|
|
788
|
+
if (radio)
|
|
789
|
+
radio.checked = true;
|
|
790
|
+
updateCategoryStyles();
|
|
791
|
+
if (commentInput) {
|
|
792
|
+
commentInput.value = '';
|
|
793
|
+
commentInput.focus();
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
const hideCommentModal = () => {
|
|
798
|
+
if (commentModal) {
|
|
799
|
+
// Track pause periods to adjust event timestamps later
|
|
800
|
+
const openedAt = globalThis.__cdpCommentModalOpenedAt;
|
|
801
|
+
if (openedAt) {
|
|
802
|
+
const closedAt = Date.now();
|
|
803
|
+
const pausePeriods = globalThis.__cdpPausePeriods || [];
|
|
804
|
+
pausePeriods.push({ start: openedAt, end: closedAt });
|
|
805
|
+
globalThis.__cdpPausePeriods = pausePeriods;
|
|
806
|
+
delete globalThis.__cdpCommentModalOpenedAt;
|
|
807
|
+
}
|
|
808
|
+
globalThis.__cdpCommentModalOpen = false;
|
|
809
|
+
commentModal.style.display = 'none';
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
const saveComment = () => {
|
|
813
|
+
const text = commentInput?.value?.trim();
|
|
814
|
+
if (text) {
|
|
815
|
+
const events = globalThis.__cdpRecordingEvents;
|
|
816
|
+
// Use the timestamp when modal was opened, not current time
|
|
817
|
+
// This way the comment appears at the right point in the timeline
|
|
818
|
+
const modalOpenedAt = globalThis.__cdpCommentModalOpenedAt || Date.now();
|
|
819
|
+
// Get selected category
|
|
820
|
+
const selectedRadio = overlay.querySelector('input[name="cdp-category"]:checked');
|
|
821
|
+
const category = selectedRadio?.value || 'narrative';
|
|
822
|
+
const commentEvent = {
|
|
823
|
+
type: 'comment',
|
|
824
|
+
text,
|
|
825
|
+
timestamp: modalOpenedAt,
|
|
826
|
+
category,
|
|
827
|
+
attachedToEventIndex: events.length > 0 ? events.length - 1 : undefined,
|
|
828
|
+
};
|
|
829
|
+
events.push(commentEvent);
|
|
830
|
+
updateOverlay(commentEvent, 'comment');
|
|
831
|
+
}
|
|
832
|
+
hideCommentModal();
|
|
833
|
+
};
|
|
834
|
+
commentBtn?.addEventListener('click', (e) => {
|
|
835
|
+
e.stopPropagation();
|
|
836
|
+
showCommentModal();
|
|
837
|
+
});
|
|
838
|
+
commentCancel?.addEventListener('click', (e) => {
|
|
839
|
+
e.stopPropagation();
|
|
840
|
+
hideCommentModal();
|
|
841
|
+
});
|
|
842
|
+
commentSave?.addEventListener('click', (e) => {
|
|
843
|
+
e.stopPropagation();
|
|
844
|
+
saveComment();
|
|
845
|
+
});
|
|
846
|
+
// Handle all keyboard events in comment input - stop them from reaching the app
|
|
847
|
+
commentInput?.addEventListener('keydown', (e) => {
|
|
848
|
+
e.stopPropagation();
|
|
849
|
+
if (e.key === 'Escape') {
|
|
850
|
+
e.preventDefault();
|
|
851
|
+
hideCommentModal();
|
|
852
|
+
}
|
|
853
|
+
else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
854
|
+
e.preventDefault();
|
|
855
|
+
saveComment();
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
commentInput?.addEventListener('keyup', (e) => {
|
|
859
|
+
e.stopPropagation();
|
|
860
|
+
});
|
|
861
|
+
commentInput?.addEventListener('keypress', (e) => {
|
|
862
|
+
e.stopPropagation();
|
|
863
|
+
});
|
|
864
|
+
// Global keyboard shortcuts for comments (Ctrl/Cmd + Shift + C/B/F)
|
|
865
|
+
const commentShortcutHandler = (e) => {
|
|
866
|
+
if (globalThis.__cdpRecordingState === 'completed')
|
|
867
|
+
return;
|
|
868
|
+
if (!(e.ctrlKey || e.metaKey) || !e.shiftKey)
|
|
869
|
+
return;
|
|
870
|
+
const key = e.key.toLowerCase();
|
|
871
|
+
let category = null;
|
|
872
|
+
if (key === 'c')
|
|
873
|
+
category = 'narrative';
|
|
874
|
+
else if (key === 'b')
|
|
875
|
+
category = 'bug';
|
|
876
|
+
else if (key === 'f')
|
|
877
|
+
category = 'feature';
|
|
878
|
+
if (category) {
|
|
879
|
+
e.preventDefault();
|
|
880
|
+
e.stopPropagation();
|
|
881
|
+
showCommentModal(category);
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
doc.addEventListener('keydown', commentShortcutHandler, { capture: true });
|
|
885
|
+
globalThis.__cdpCommentShortcutHandler = commentShortcutHandler;
|
|
886
|
+
pauseBtn?.addEventListener('click', (e) => {
|
|
887
|
+
e.stopPropagation();
|
|
888
|
+
const isPaused = !globalThis.__cdpRecordingPaused;
|
|
889
|
+
globalThis.__cdpRecordingPaused = isPaused;
|
|
890
|
+
pauseBtn.textContent = isPaused ? '▶' : '⏸';
|
|
891
|
+
const statusEl = overlay.querySelector('.cdp-status');
|
|
892
|
+
if (statusEl) {
|
|
893
|
+
statusEl.textContent = isPaused ? 'PAUSED' : 'REC';
|
|
894
|
+
statusEl.style.background = isPaused ? '#eab308' : '#ef4444';
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
resetBtn?.addEventListener('click', (e) => {
|
|
898
|
+
e.stopPropagation();
|
|
899
|
+
globalThis.__cdpRecordingEvents = [];
|
|
900
|
+
globalThis.__cdpRecordingStart = Date.now();
|
|
901
|
+
const statsEl = overlay.querySelector('.cdp-stats');
|
|
902
|
+
if (statsEl)
|
|
903
|
+
statsEl.textContent = '0 | 0.0s';
|
|
904
|
+
});
|
|
905
|
+
doneBtn?.addEventListener('click', (e) => {
|
|
906
|
+
e.stopPropagation();
|
|
907
|
+
globalThis.__cdpRecordingState = 'completed';
|
|
908
|
+
// Send events to server via CDP binding
|
|
909
|
+
const events = globalThis.__cdpRecordingEvents || [];
|
|
910
|
+
const pausePeriods = globalThis.__cdpPausePeriods || [];
|
|
911
|
+
try {
|
|
912
|
+
globalThis.__cdpRecordingComplete(JSON.stringify({ events, pausePeriods }));
|
|
913
|
+
}
|
|
914
|
+
catch (err) {
|
|
915
|
+
console.error('[cdp-tools] Failed to send recording to server:', err);
|
|
916
|
+
}
|
|
917
|
+
const panel = overlay.querySelector('.cdp-panel');
|
|
918
|
+
if (panel) {
|
|
919
|
+
panel.style.background = 'rgba(5, 150, 105, 0.9)';
|
|
920
|
+
panel.textContent = '';
|
|
921
|
+
const span = doc.createElement('span');
|
|
922
|
+
span.style.padding = '4px 8px';
|
|
923
|
+
span.textContent = 'Saved';
|
|
924
|
+
panel.appendChild(span);
|
|
925
|
+
// Remove overlay after brief delay
|
|
926
|
+
setTimeout(() => {
|
|
927
|
+
overlay.remove();
|
|
928
|
+
}, 800);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
cancelBtn?.addEventListener('click', (e) => {
|
|
932
|
+
e.stopPropagation();
|
|
933
|
+
globalThis.__cdpRecordingState = 'cancelled';
|
|
934
|
+
// Send cancel signal to server via CDP binding
|
|
935
|
+
// Note: CDP bindings require exactly one string argument
|
|
936
|
+
try {
|
|
937
|
+
globalThis.__cdpRecordingCancel('cancel');
|
|
938
|
+
}
|
|
939
|
+
catch (err) {
|
|
940
|
+
console.error('[cdp-tools] Failed to send cancel to server:', err);
|
|
941
|
+
}
|
|
942
|
+
const panel = overlay.querySelector('.cdp-panel');
|
|
943
|
+
if (panel) {
|
|
944
|
+
panel.style.background = 'rgba(220, 38, 38, 0.9)';
|
|
945
|
+
panel.textContent = '';
|
|
946
|
+
const span = doc.createElement('span');
|
|
947
|
+
span.style.padding = '4px 8px';
|
|
948
|
+
span.textContent = 'Cancelled';
|
|
949
|
+
panel.appendChild(span);
|
|
950
|
+
// Remove overlay after brief delay
|
|
951
|
+
setTimeout(() => {
|
|
952
|
+
overlay.remove();
|
|
953
|
+
}, 800);
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
globalThis.__cdpOverlay = overlay;
|
|
957
|
+
}
|
|
958
|
+
// Store listener references
|
|
959
|
+
globalThis.__cdpRecordingListeners = {
|
|
960
|
+
mousemove: (e) => captureMouseEvent(e, 'mousemove'),
|
|
961
|
+
mousedown: (e) => captureMouseEvent(e, 'mousedown'),
|
|
962
|
+
mouseup: (e) => captureMouseEvent(e, 'mouseup'),
|
|
963
|
+
wheel: (e) => captureMouseEvent(e, 'wheel'),
|
|
964
|
+
click: (e) => captureMouseEvent(e, 'click'),
|
|
965
|
+
dblclick: (e) => captureMouseEvent(e, 'dblclick'),
|
|
966
|
+
keydown: (e) => captureKeyEvent(e, 'keydown'),
|
|
967
|
+
keyup: (e) => captureKeyEvent(e, 'keyup'),
|
|
968
|
+
paste: (e) => capturePasteEvent(e),
|
|
969
|
+
};
|
|
970
|
+
const listeners = globalThis.__cdpRecordingListeners;
|
|
971
|
+
doc.addEventListener('mousemove', listeners.mousemove, { capture: true, passive: true });
|
|
972
|
+
doc.addEventListener('mousedown', listeners.mousedown, { capture: true, passive: true });
|
|
973
|
+
doc.addEventListener('mouseup', listeners.mouseup, { capture: true, passive: true });
|
|
974
|
+
doc.addEventListener('wheel', listeners.wheel, { capture: true, passive: true });
|
|
975
|
+
doc.addEventListener('click', listeners.click, { capture: true, passive: true });
|
|
976
|
+
doc.addEventListener('dblclick', listeners.dblclick, { capture: true, passive: true });
|
|
977
|
+
doc.addEventListener('keydown', listeners.keydown, { capture: true, passive: true });
|
|
978
|
+
doc.addEventListener('keyup', listeners.keyup, { capture: true, passive: true });
|
|
979
|
+
doc.addEventListener('paste', listeners.paste, { capture: true });
|
|
980
|
+
}, showOverlayOption);
|
|
981
|
+
};
|
|
982
|
+
// Track if we've done initial injection (to distinguish from navigation reloads)
|
|
983
|
+
let initialInjectionDone = false;
|
|
984
|
+
// Re-inject recording overlay and listeners after page load (handles refresh/navigation)
|
|
985
|
+
client.on('Page.loadEventFired', async () => {
|
|
986
|
+
// Skip the initial load event since we inject manually below
|
|
987
|
+
if (!initialInjectionDone) {
|
|
988
|
+
debugLog('recording', 'Skipping initial Page.loadEventFired');
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
debugLog('recording', 'Page.loadEventFired - re-registering bindings and re-injecting script');
|
|
992
|
+
// Small delay to ensure page is ready
|
|
993
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
994
|
+
try {
|
|
995
|
+
// Re-register bindings (they don't persist across navigations)
|
|
996
|
+
await client.send('Runtime.addBinding', { name: '__cdpRecordingComplete' });
|
|
997
|
+
await client.send('Runtime.addBinding', { name: '__cdpRecordingCancel' });
|
|
998
|
+
await client.send('Runtime.addBinding', { name: '__cdpRecordingSaveEvents' });
|
|
999
|
+
debugLog('recording', 'Bindings re-registered');
|
|
1000
|
+
await injectRecordingScript();
|
|
1001
|
+
debugLog('recording', 'Recording script re-injected successfully');
|
|
1002
|
+
}
|
|
1003
|
+
catch (e) {
|
|
1004
|
+
// Page may have been closed or navigated away
|
|
1005
|
+
debugLog('recording', `Failed to re-inject recording script: ${e}`);
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
// Initial injection
|
|
1009
|
+
await injectRecordingScript();
|
|
1010
|
+
initialInjectionDone = true;
|
|
1011
|
+
// Store cleanup function
|
|
1012
|
+
cleanupHandles.set(connectionReference, async () => {
|
|
1013
|
+
try {
|
|
1014
|
+
await client.detach();
|
|
1015
|
+
}
|
|
1016
|
+
catch (e) { /* ignore */ }
|
|
1017
|
+
try {
|
|
1018
|
+
await page.evaluate(() => {
|
|
1019
|
+
const listeners = globalThis.__cdpRecordingListeners;
|
|
1020
|
+
const doc = globalThis.document;
|
|
1021
|
+
if (listeners) {
|
|
1022
|
+
doc.removeEventListener('mousemove', listeners.mousemove, { capture: true });
|
|
1023
|
+
doc.removeEventListener('mousedown', listeners.mousedown, { capture: true });
|
|
1024
|
+
doc.removeEventListener('mouseup', listeners.mouseup, { capture: true });
|
|
1025
|
+
doc.removeEventListener('wheel', listeners.wheel, { capture: true });
|
|
1026
|
+
doc.removeEventListener('click', listeners.click, { capture: true });
|
|
1027
|
+
doc.removeEventListener('dblclick', listeners.dblclick, { capture: true });
|
|
1028
|
+
doc.removeEventListener('keydown', listeners.keydown, { capture: true });
|
|
1029
|
+
doc.removeEventListener('keyup', listeners.keyup, { capture: true });
|
|
1030
|
+
doc.removeEventListener('paste', listeners.paste, { capture: true });
|
|
1031
|
+
}
|
|
1032
|
+
const overlay = doc.getElementById('__cdp-recording-overlay');
|
|
1033
|
+
if (overlay)
|
|
1034
|
+
overlay.remove();
|
|
1035
|
+
delete globalThis.__cdpRecordingEvents;
|
|
1036
|
+
delete globalThis.__cdpRecordingListeners;
|
|
1037
|
+
delete globalThis.__cdpRecordingStart;
|
|
1038
|
+
delete globalThis.__cdpRecordingPaused;
|
|
1039
|
+
delete globalThis.__cdpRecordingState;
|
|
1040
|
+
delete globalThis.__cdpOverlay;
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
catch (e) { /* page may have navigated */ }
|
|
1044
|
+
});
|
|
1045
|
+
// Block until recording completes (via UI button or stopRecording call)
|
|
1046
|
+
return recordingPromise;
|
|
1047
|
+
}
|
|
1048
|
+
catch (error) {
|
|
1049
|
+
activeSessions.delete(connectionReference);
|
|
1050
|
+
return { success: false, error: error.message || String(error) };
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
// =============================================================================
|
|
1054
|
+
// Helper Functions
|
|
1055
|
+
// =============================================================================
|
|
1056
|
+
export function calculateSummary(events) {
|
|
1057
|
+
let clicks = 0;
|
|
1058
|
+
let drags = 0;
|
|
1059
|
+
let scrolls = 0;
|
|
1060
|
+
let keyPresses = 0;
|
|
1061
|
+
let navigations = 0;
|
|
1062
|
+
let comments = 0;
|
|
1063
|
+
let selectorsAvailable = 0;
|
|
1064
|
+
let coordinatesOnly = 0;
|
|
1065
|
+
let canvasInteractions = 0;
|
|
1066
|
+
let interactiveElements = 0;
|
|
1067
|
+
let typedCharacters = 0;
|
|
1068
|
+
let inDrag = false;
|
|
1069
|
+
let lastMouseDown = null;
|
|
1070
|
+
for (const event of events) {
|
|
1071
|
+
if (isCommentEvent(event)) {
|
|
1072
|
+
comments++;
|
|
1073
|
+
}
|
|
1074
|
+
else if (isNavigationEvent(event)) {
|
|
1075
|
+
navigations++;
|
|
1076
|
+
}
|
|
1077
|
+
else if (isKeyboardEvent(event)) {
|
|
1078
|
+
if (event.type === 'keydown') {
|
|
1079
|
+
keyPresses++;
|
|
1080
|
+
// Single-character keys are typed text; the rest are navigation and
|
|
1081
|
+
// modifier presses (Tab, Enter, Shift, arrows).
|
|
1082
|
+
if (event.key.length === 1)
|
|
1083
|
+
typedCharacters++;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
else if (isMouseEvent(event)) {
|
|
1087
|
+
if (event.type === 'mousedown') {
|
|
1088
|
+
inDrag = true;
|
|
1089
|
+
lastMouseDown = event;
|
|
1090
|
+
}
|
|
1091
|
+
else if (event.type === 'mouseup' && inDrag && lastMouseDown) {
|
|
1092
|
+
const distance = Math.sqrt(Math.pow(event.x - lastMouseDown.x, 2) + Math.pow(event.y - lastMouseDown.y, 2));
|
|
1093
|
+
if (distance > 5)
|
|
1094
|
+
drags++;
|
|
1095
|
+
inDrag = false;
|
|
1096
|
+
}
|
|
1097
|
+
else if (event.type === 'click') {
|
|
1098
|
+
clicks++;
|
|
1099
|
+
// Coverage is only meaningful for clicks - a keypress goes to whatever
|
|
1100
|
+
// has focus, so it has no selector to fall back from.
|
|
1101
|
+
if (event.elementInfo?.isCanvas) {
|
|
1102
|
+
// A canvas click has no meaningful element to target, so it is
|
|
1103
|
+
// coordinates by necessity rather than by failure to find a
|
|
1104
|
+
// selector. Counted in both, since it is still coordinate-fragile.
|
|
1105
|
+
canvasInteractions++;
|
|
1106
|
+
coordinatesOnly++;
|
|
1107
|
+
}
|
|
1108
|
+
else if (event.elementInfo?.selector) {
|
|
1109
|
+
selectorsAvailable++;
|
|
1110
|
+
if (event.elementInfo.isInteractive)
|
|
1111
|
+
interactiveElements++;
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
coordinatesOnly++;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
else if (event.type === 'wheel') {
|
|
1118
|
+
scrolls++;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return {
|
|
1123
|
+
clicks, drags, scrolls, keyPresses, navigations, comments,
|
|
1124
|
+
selectorsAvailable, coordinatesOnly, canvasInteractions,
|
|
1125
|
+
interactiveElements, typedCharacters,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
// =============================================================================
|
|
1129
|
+
// Event Processing
|
|
1130
|
+
// =============================================================================
|
|
1131
|
+
export function simplifyEvents(events, options = {}) {
|
|
1132
|
+
const { distanceThreshold = 10, timeThreshold = 100, scrollTimeThreshold = 150 } = options;
|
|
1133
|
+
const simplified = [];
|
|
1134
|
+
let lastMove = null;
|
|
1135
|
+
let pendingScroll = null;
|
|
1136
|
+
const flushPending = () => {
|
|
1137
|
+
if (lastMove) {
|
|
1138
|
+
simplified.push(lastMove);
|
|
1139
|
+
lastMove = null;
|
|
1140
|
+
}
|
|
1141
|
+
if (pendingScroll) {
|
|
1142
|
+
simplified.push(pendingScroll);
|
|
1143
|
+
pendingScroll = null;
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
for (const event of events) {
|
|
1147
|
+
if (isKeyboardEvent(event) || isNavigationEvent(event) || isCommentEvent(event) || isPasteEvent(event)) {
|
|
1148
|
+
flushPending();
|
|
1149
|
+
simplified.push(event);
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
if (isMouseEvent(event)) {
|
|
1153
|
+
if (event.type === 'wheel') {
|
|
1154
|
+
// Consolidate consecutive wheel events within time threshold
|
|
1155
|
+
if (lastMove) {
|
|
1156
|
+
simplified.push(lastMove);
|
|
1157
|
+
lastMove = null;
|
|
1158
|
+
}
|
|
1159
|
+
if (!pendingScroll) {
|
|
1160
|
+
pendingScroll = { ...event };
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
const timeDiff = event.timestamp - pendingScroll.timestamp;
|
|
1164
|
+
if (timeDiff <= scrollTimeThreshold) {
|
|
1165
|
+
// Accumulate scroll deltas
|
|
1166
|
+
pendingScroll.deltaX = (pendingScroll.deltaX || 0) + (event.deltaX || 0);
|
|
1167
|
+
pendingScroll.deltaY = (pendingScroll.deltaY || 0) + (event.deltaY || 0);
|
|
1168
|
+
pendingScroll.timestamp = event.timestamp; // Update to latest timestamp
|
|
1169
|
+
}
|
|
1170
|
+
else {
|
|
1171
|
+
// Time gap too large, push previous and start new
|
|
1172
|
+
simplified.push(pendingScroll);
|
|
1173
|
+
pendingScroll = { ...event };
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
else if (event.type === 'mousemove') {
|
|
1178
|
+
if (pendingScroll) {
|
|
1179
|
+
simplified.push(pendingScroll);
|
|
1180
|
+
pendingScroll = null;
|
|
1181
|
+
}
|
|
1182
|
+
if (!lastMove) {
|
|
1183
|
+
lastMove = event;
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
const distance = Math.sqrt(Math.pow(event.x - lastMove.x, 2) + Math.pow(event.y - lastMove.y, 2));
|
|
1187
|
+
const timeDiff = event.timestamp - lastMove.timestamp;
|
|
1188
|
+
if (distance >= distanceThreshold || timeDiff >= timeThreshold) {
|
|
1189
|
+
simplified.push(lastMove);
|
|
1190
|
+
lastMove = event;
|
|
1191
|
+
}
|
|
1192
|
+
else {
|
|
1193
|
+
lastMove = event;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
// click, mousedown, mouseup, dblclick
|
|
1199
|
+
flushPending();
|
|
1200
|
+
simplified.push(event);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
flushPending();
|
|
1205
|
+
return simplified;
|
|
1206
|
+
}
|
|
1207
|
+
export function eventsToCommands(events, options = {}) {
|
|
1208
|
+
const { simplify = true, includeHovers = false, preferCoordinates = false, preferSelectors = false, includeDelays = false, startTime, maxDelayMs = 0 } = options;
|
|
1209
|
+
const processedEvents = simplify ? simplifyEvents(events) : events;
|
|
1210
|
+
const commands = [];
|
|
1211
|
+
const baseTime = startTime || (processedEvents.length > 0 ? processedEvents[0].timestamp : 0);
|
|
1212
|
+
let lastTimestamp = baseTime;
|
|
1213
|
+
// Track the last selector that received typed text (for append mode)
|
|
1214
|
+
let lastTypedSelector = null;
|
|
1215
|
+
const addCommand = (cmd, eventTimestamp) => {
|
|
1216
|
+
if (includeDelays) {
|
|
1217
|
+
let delay = eventTimestamp - lastTimestamp;
|
|
1218
|
+
// Cap delay at maxDelayMs if configured (0 = no limit)
|
|
1219
|
+
if (maxDelayMs > 0 && delay > maxDelayMs) {
|
|
1220
|
+
delay = maxDelayMs;
|
|
1221
|
+
}
|
|
1222
|
+
if (delay > 0) {
|
|
1223
|
+
commands.push({ ...cmd, delay });
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
commands.push(cmd);
|
|
1227
|
+
}
|
|
1228
|
+
lastTimestamp = eventTimestamp;
|
|
1229
|
+
}
|
|
1230
|
+
else {
|
|
1231
|
+
commands.push(cmd);
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
let i = 0;
|
|
1235
|
+
while (i < processedEvents.length) {
|
|
1236
|
+
const event = processedEvents[i];
|
|
1237
|
+
// Comment events - attach to the last meaningful command (skip modifier-only keys)
|
|
1238
|
+
if (isCommentEvent(event)) {
|
|
1239
|
+
// Find the last meaningful command (not a modifier-only keypress)
|
|
1240
|
+
for (let j = commands.length - 1; j >= 0; j--) {
|
|
1241
|
+
const cmd = commands[j];
|
|
1242
|
+
// Skip modifier-only keypresses like Meta+Meta, Shift+Meta+Shift
|
|
1243
|
+
if (cmd.tool === 'input' && cmd.params.action === 'press') {
|
|
1244
|
+
const key = cmd.params.key;
|
|
1245
|
+
if (key && /^(Meta|Shift|Control|Alt)(\+(Meta|Shift|Control|Alt))*$/.test(key)) {
|
|
1246
|
+
continue; // Skip this command, look for previous
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
// Found a meaningful command - attach comment
|
|
1250
|
+
if (cmd.comment) {
|
|
1251
|
+
cmd.comment += '\n' + event.text;
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
cmd.comment = event.text;
|
|
1255
|
+
}
|
|
1256
|
+
break;
|
|
1257
|
+
}
|
|
1258
|
+
i++;
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
// Navigation events
|
|
1262
|
+
if (isNavigationEvent(event)) {
|
|
1263
|
+
if (event.type === 'navigation') {
|
|
1264
|
+
addCommand({ tool: 'navigate', params: { action: 'goto', url: event.url } }, event.timestamp);
|
|
1265
|
+
}
|
|
1266
|
+
else if (event.type === 'reload') {
|
|
1267
|
+
addCommand({ tool: 'navigate', params: { action: 'reload' } }, event.timestamp);
|
|
1268
|
+
}
|
|
1269
|
+
i++;
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
// Paste events - convert to type command with the pasted text
|
|
1273
|
+
if (isPasteEvent(event)) {
|
|
1274
|
+
// Remove any preceding paste-shortcut command (Meta+v or Ctrl+v)
|
|
1275
|
+
// that was captured before this paste event
|
|
1276
|
+
if (commands.length > 0) {
|
|
1277
|
+
const lastCmd = commands[commands.length - 1];
|
|
1278
|
+
if (lastCmd.tool === 'input' && lastCmd.params.action === 'press') {
|
|
1279
|
+
const key = (lastCmd.params.key || '').toLowerCase();
|
|
1280
|
+
if (key === 'meta+v' || key === 'control+v' || key === 'ctrl+v') {
|
|
1281
|
+
commands.pop(); // Remove the paste shortcut command
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
// Include selector if available from the paste target
|
|
1286
|
+
const selector = event.targetInfo?.selector;
|
|
1287
|
+
const typeParams = { action: 'type', text: event.text };
|
|
1288
|
+
if (selector) {
|
|
1289
|
+
typeParams.selector = selector;
|
|
1290
|
+
// Append if we already typed to this selector
|
|
1291
|
+
if (lastTypedSelector === selector) {
|
|
1292
|
+
typeParams.append = true;
|
|
1293
|
+
}
|
|
1294
|
+
lastTypedSelector = selector;
|
|
1295
|
+
}
|
|
1296
|
+
addCommand({ tool: 'input', params: typeParams }, event.timestamp);
|
|
1297
|
+
i++;
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
// Keyboard events
|
|
1301
|
+
if (isKeyboardEvent(event)) {
|
|
1302
|
+
if (event.type === 'keydown') {
|
|
1303
|
+
if (event.key.length === 1 && !event.modifiers?.ctrl && !event.modifiers?.alt && !event.modifiers?.meta) {
|
|
1304
|
+
let typedText = event.key;
|
|
1305
|
+
// Use selector from first event in the sequence
|
|
1306
|
+
const selector = event.targetInfo?.selector;
|
|
1307
|
+
let j = i + 1;
|
|
1308
|
+
while (j < processedEvents.length) {
|
|
1309
|
+
const next = processedEvents[j];
|
|
1310
|
+
if (isKeyboardEvent(next) && next.type === 'keydown' &&
|
|
1311
|
+
next.key.length === 1 && !next.modifiers?.ctrl && !next.modifiers?.alt && !next.modifiers?.meta) {
|
|
1312
|
+
typedText += next.key;
|
|
1313
|
+
j++;
|
|
1314
|
+
}
|
|
1315
|
+
else if (isKeyboardEvent(next) && next.type === 'keyup') {
|
|
1316
|
+
j++;
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (typedText.length > 1) {
|
|
1323
|
+
const typeParams = { action: 'type', text: typedText };
|
|
1324
|
+
if (selector) {
|
|
1325
|
+
typeParams.selector = selector;
|
|
1326
|
+
// Append if we already typed to this selector
|
|
1327
|
+
if (lastTypedSelector === selector) {
|
|
1328
|
+
typeParams.append = true;
|
|
1329
|
+
}
|
|
1330
|
+
lastTypedSelector = selector;
|
|
1331
|
+
}
|
|
1332
|
+
addCommand({ tool: 'input', params: typeParams }, event.timestamp);
|
|
1333
|
+
i = j;
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
const key = event.key;
|
|
1338
|
+
const modifiers = event.modifiers;
|
|
1339
|
+
let keyCombo = key;
|
|
1340
|
+
if (modifiers) {
|
|
1341
|
+
const parts = [];
|
|
1342
|
+
if (modifiers.ctrl)
|
|
1343
|
+
parts.push('Control');
|
|
1344
|
+
if (modifiers.alt)
|
|
1345
|
+
parts.push('Alt');
|
|
1346
|
+
if (modifiers.shift && key.length > 1)
|
|
1347
|
+
parts.push('Shift');
|
|
1348
|
+
if (modifiers.meta)
|
|
1349
|
+
parts.push('Meta');
|
|
1350
|
+
if (parts.length > 0)
|
|
1351
|
+
keyCombo = parts.join('+') + '+' + key;
|
|
1352
|
+
}
|
|
1353
|
+
addCommand({ tool: 'input', params: { action: 'press', key: keyCombo } }, event.timestamp);
|
|
1354
|
+
}
|
|
1355
|
+
i++;
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
// Mouse events
|
|
1359
|
+
if (!isMouseEvent(event)) {
|
|
1360
|
+
i++;
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
// Drag detection
|
|
1364
|
+
if (event.type === 'mousedown') {
|
|
1365
|
+
const dragStart = { x: Math.max(0, event.x), y: Math.max(0, event.y) };
|
|
1366
|
+
let dragEnd = dragStart;
|
|
1367
|
+
let j = i + 1;
|
|
1368
|
+
while (j < processedEvents.length && processedEvents[j].type !== 'mouseup') {
|
|
1369
|
+
const nextEvent = processedEvents[j];
|
|
1370
|
+
if (isMouseEvent(nextEvent) && nextEvent.type === 'mousemove') {
|
|
1371
|
+
// Clamp coordinates to valid viewport bounds (negative coords are outside viewport)
|
|
1372
|
+
dragEnd = { x: Math.max(0, nextEvent.x), y: Math.max(0, nextEvent.y) };
|
|
1373
|
+
}
|
|
1374
|
+
j++;
|
|
1375
|
+
}
|
|
1376
|
+
if (j < processedEvents.length && processedEvents[j].type === 'mouseup') {
|
|
1377
|
+
const mouseup = processedEvents[j];
|
|
1378
|
+
if (isMouseEvent(mouseup)) {
|
|
1379
|
+
// Clamp coordinates to valid viewport bounds
|
|
1380
|
+
dragEnd = { x: Math.max(0, mouseup.x), y: Math.max(0, mouseup.y) };
|
|
1381
|
+
}
|
|
1382
|
+
const distance = Math.sqrt(Math.pow(dragEnd.x - dragStart.x, 2) + Math.pow(dragEnd.y - dragStart.y, 2));
|
|
1383
|
+
if (distance > 5) {
|
|
1384
|
+
addCommand({ tool: 'input', params: { action: 'drag', from: dragStart, to: dragEnd } }, event.timestamp);
|
|
1385
|
+
}
|
|
1386
|
+
i = j + 1;
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
// Scroll
|
|
1391
|
+
if (event.type === 'wheel' && (event.deltaX || event.deltaY)) {
|
|
1392
|
+
addCommand({
|
|
1393
|
+
tool: 'input',
|
|
1394
|
+
params: { action: 'scroll', x: event.x, y: event.y, deltaX: event.deltaX || 0, deltaY: event.deltaY || 0 },
|
|
1395
|
+
}, event.timestamp);
|
|
1396
|
+
i++;
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
// Click
|
|
1400
|
+
if (event.type === 'click') {
|
|
1401
|
+
const elementInfo = event.elementInfo;
|
|
1402
|
+
const selector = elementInfo?.selector;
|
|
1403
|
+
const isCanvas = elementInfo?.isCanvas;
|
|
1404
|
+
// Use selector when available, unless preferCoordinates is set or element is canvas.
|
|
1405
|
+
// Selector-based clicks are more reliable as they survive layout changes.
|
|
1406
|
+
// preferSelectors forces the selector whenever one exists (canvas included)
|
|
1407
|
+
// and wins over preferCoordinates when both flags are set.
|
|
1408
|
+
const useSelector = selector && (preferSelectors || (!preferCoordinates && !isCanvas));
|
|
1409
|
+
if (useSelector) {
|
|
1410
|
+
addCommand({ tool: 'input', params: { action: 'click', selector } }, event.timestamp);
|
|
1411
|
+
}
|
|
1412
|
+
else {
|
|
1413
|
+
// Coordinate-based click for canvas/3D elements or when no selector available
|
|
1414
|
+
addCommand({ tool: 'input', params: { action: 'click', x: event.x, y: event.y } }, event.timestamp);
|
|
1415
|
+
}
|
|
1416
|
+
// Reset lastTypedSelector on click - typing after a click should replace, not append
|
|
1417
|
+
lastTypedSelector = null;
|
|
1418
|
+
i++;
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
// Mousemove (hover)
|
|
1422
|
+
if (event.type === 'mousemove' && includeHovers) {
|
|
1423
|
+
addCommand({ tool: 'input', params: { action: 'mousemove', x: event.x, y: event.y } }, event.timestamp);
|
|
1424
|
+
}
|
|
1425
|
+
i++;
|
|
1426
|
+
}
|
|
1427
|
+
return commands;
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Generate a condensed timeline of events, grouping consecutive actions of the same type
|
|
1431
|
+
* but showing actual comment text inline.
|
|
1432
|
+
* Example: "3 clicks → 5 keys → 'testing this feature' → 2 clicks → 'done testing'"
|
|
1433
|
+
*/
|
|
1434
|
+
export function generateCondensedTimeline(events) {
|
|
1435
|
+
const simplified = simplifyEvents(events);
|
|
1436
|
+
const parts = [];
|
|
1437
|
+
let currentType = null;
|
|
1438
|
+
let currentCount = 0;
|
|
1439
|
+
const flushCurrent = () => {
|
|
1440
|
+
if (currentType && currentCount > 0) {
|
|
1441
|
+
if (currentCount === 1) {
|
|
1442
|
+
parts.push(`1 ${currentType}`);
|
|
1443
|
+
}
|
|
1444
|
+
else {
|
|
1445
|
+
parts.push(`${currentCount} ${currentType}s`);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
currentType = null;
|
|
1449
|
+
currentCount = 0;
|
|
1450
|
+
};
|
|
1451
|
+
for (const event of simplified) {
|
|
1452
|
+
if (isCommentEvent(event)) {
|
|
1453
|
+
flushCurrent();
|
|
1454
|
+
// Truncate long comments
|
|
1455
|
+
const text = event.text.length > 50 ? event.text.substring(0, 47) + '...' : event.text;
|
|
1456
|
+
const category = event.category || 'narrative';
|
|
1457
|
+
if (category === 'bug') {
|
|
1458
|
+
parts.push(`BUG: "${text}"`);
|
|
1459
|
+
}
|
|
1460
|
+
else if (category === 'feature') {
|
|
1461
|
+
parts.push(`FEATURE: "${text}"`);
|
|
1462
|
+
}
|
|
1463
|
+
else {
|
|
1464
|
+
parts.push(`"${text}"`);
|
|
1465
|
+
}
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
if (isNavigationEvent(event)) {
|
|
1469
|
+
flushCurrent();
|
|
1470
|
+
parts.push(`nav`);
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
if (isPasteEvent(event)) {
|
|
1474
|
+
flushCurrent();
|
|
1475
|
+
// Show truncated paste content
|
|
1476
|
+
const text = event.text.length > 30 ? event.text.substring(0, 27) + '...' : event.text;
|
|
1477
|
+
parts.push(`paste: "${text}"`);
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
let eventType;
|
|
1481
|
+
if (isKeyboardEvent(event)) {
|
|
1482
|
+
if (event.type === 'keyup')
|
|
1483
|
+
continue;
|
|
1484
|
+
eventType = 'key';
|
|
1485
|
+
}
|
|
1486
|
+
else if (isMouseEvent(event)) {
|
|
1487
|
+
if (event.type === 'click') {
|
|
1488
|
+
eventType = 'click';
|
|
1489
|
+
}
|
|
1490
|
+
else if (event.type === 'wheel') {
|
|
1491
|
+
eventType = 'scroll';
|
|
1492
|
+
}
|
|
1493
|
+
else if (event.type === 'mousemove') {
|
|
1494
|
+
continue; // Skip mousemove
|
|
1495
|
+
}
|
|
1496
|
+
else {
|
|
1497
|
+
continue; // Skip mousedown/mouseup
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
else {
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
if (eventType === currentType) {
|
|
1504
|
+
currentCount++;
|
|
1505
|
+
}
|
|
1506
|
+
else {
|
|
1507
|
+
flushCurrent();
|
|
1508
|
+
currentType = eventType;
|
|
1509
|
+
currentCount = 1;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
flushCurrent();
|
|
1513
|
+
return parts.join(' → ');
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Show a verification overlay on the page asking the user to confirm
|
|
1517
|
+
* whether an issue is resolved.
|
|
1518
|
+
*/
|
|
1519
|
+
export async function showVerificationOverlay(page, issueType, issueTitle, issueId) {
|
|
1520
|
+
const typeLabel = issueType === 'bug' ? 'Bug' : 'Feature';
|
|
1521
|
+
const questionText = issueType === 'bug'
|
|
1522
|
+
? 'Is this bug fixed to your satisfaction?'
|
|
1523
|
+
: 'Is this feature implemented to your satisfaction?';
|
|
1524
|
+
return await page.evaluate((params) => {
|
|
1525
|
+
return new Promise((resolve) => {
|
|
1526
|
+
const doc = globalThis.document;
|
|
1527
|
+
// Remove any existing overlays (including replay overlay that may still be present)
|
|
1528
|
+
const existing = doc.getElementById('__cdp-verification-overlay');
|
|
1529
|
+
if (existing)
|
|
1530
|
+
existing.remove();
|
|
1531
|
+
const replayOverlay = doc.getElementById('__cdp-replay-overlay');
|
|
1532
|
+
if (replayOverlay)
|
|
1533
|
+
replayOverlay.remove();
|
|
1534
|
+
// Also clean up replay overlay event listeners if present
|
|
1535
|
+
const blocker = globalThis.__cdpReplayBlocker;
|
|
1536
|
+
if (blocker) {
|
|
1537
|
+
blocker.feedbackEvents?.forEach((evt) => doc.removeEventListener(evt, blocker.blockWithFeedback, true));
|
|
1538
|
+
blocker.silentBlockEvents?.forEach((evt) => doc.removeEventListener(evt, blocker.blockSilently, true));
|
|
1539
|
+
delete globalThis.__cdpReplayBlocker;
|
|
1540
|
+
}
|
|
1541
|
+
const style = globalThis.__cdpReplayStyle;
|
|
1542
|
+
if (style) {
|
|
1543
|
+
style.remove();
|
|
1544
|
+
delete globalThis.__cdpReplayStyle;
|
|
1545
|
+
}
|
|
1546
|
+
// Create elements using DOM methods (Trusted Types compatible)
|
|
1547
|
+
const overlay = doc.createElement('div');
|
|
1548
|
+
overlay.id = '__cdp-verification-overlay';
|
|
1549
|
+
overlay.style.cssText = 'opacity: 1 !important;';
|
|
1550
|
+
const backdrop = doc.createElement('div');
|
|
1551
|
+
backdrop.style.cssText = `
|
|
1552
|
+
position: fixed !important;
|
|
1553
|
+
top: 0 !important;
|
|
1554
|
+
left: 0 !important;
|
|
1555
|
+
right: 0 !important;
|
|
1556
|
+
bottom: 0 !important;
|
|
1557
|
+
background: #424242 !important;
|
|
1558
|
+
z-index: 2147483646 !important;
|
|
1559
|
+
display: flex !important;
|
|
1560
|
+
align-items: center !important;
|
|
1561
|
+
justify-content: center !important;
|
|
1562
|
+
font-family: Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
|
|
1563
|
+
opacity: 1 !important;
|
|
1564
|
+
`;
|
|
1565
|
+
const card = doc.createElement('div');
|
|
1566
|
+
card.style.cssText = `
|
|
1567
|
+
background: #ffffff !important;
|
|
1568
|
+
border-radius: 4px !important;
|
|
1569
|
+
padding: 24px !important;
|
|
1570
|
+
max-width: 480px !important;
|
|
1571
|
+
width: 90% !important;
|
|
1572
|
+
box-shadow: 0 11px 15px -7px rgba(0,0,0,0.2), 0 24px 38px 3px rgba(0,0,0,0.14), 0 9px 46px 8px rgba(0,0,0,0.12) !important;
|
|
1573
|
+
color: #212121 !important;
|
|
1574
|
+
opacity: 1 !important;
|
|
1575
|
+
`;
|
|
1576
|
+
// Header
|
|
1577
|
+
const header = doc.createElement('div');
|
|
1578
|
+
header.style.cssText = 'margin-bottom: 16px !important; text-align: center !important; opacity: 1 !important;';
|
|
1579
|
+
const typeLabel = doc.createElement('div');
|
|
1580
|
+
typeLabel.textContent = `${params.typeLabel} #${params.issueId}`;
|
|
1581
|
+
typeLabel.style.cssText = `
|
|
1582
|
+
font-size: 12px !important;
|
|
1583
|
+
color: #757575 !important;
|
|
1584
|
+
text-transform: uppercase !important;
|
|
1585
|
+
letter-spacing: 0.5px !important;
|
|
1586
|
+
margin-bottom: 8px !important;
|
|
1587
|
+
opacity: 1 !important;
|
|
1588
|
+
`;
|
|
1589
|
+
const description = doc.createElement('div');
|
|
1590
|
+
description.textContent = params.issueTitle;
|
|
1591
|
+
description.style.cssText = `
|
|
1592
|
+
font-size: 20px !important;
|
|
1593
|
+
font-weight: 500 !important;
|
|
1594
|
+
color: #212121 !important;
|
|
1595
|
+
line-height: 1.4 !important;
|
|
1596
|
+
text-align: center !important;
|
|
1597
|
+
opacity: 1 !important;
|
|
1598
|
+
`;
|
|
1599
|
+
header.appendChild(typeLabel);
|
|
1600
|
+
header.appendChild(description);
|
|
1601
|
+
// Question text (subtle styling)
|
|
1602
|
+
const questionBox = doc.createElement('div');
|
|
1603
|
+
questionBox.textContent = params.questionText;
|
|
1604
|
+
questionBox.style.cssText = `
|
|
1605
|
+
font-size: 14px !important;
|
|
1606
|
+
margin-bottom: 20px !important;
|
|
1607
|
+
padding: 12px 16px !important;
|
|
1608
|
+
background: #fafafa !important;
|
|
1609
|
+
border-radius: 4px !important;
|
|
1610
|
+
color: #757575 !important;
|
|
1611
|
+
opacity: 1 !important;
|
|
1612
|
+
`;
|
|
1613
|
+
// Comment textarea
|
|
1614
|
+
const commentInput = doc.createElement('textarea');
|
|
1615
|
+
commentInput.id = '__cdp-verify-comment';
|
|
1616
|
+
commentInput.placeholder = 'Add a comment (optional)';
|
|
1617
|
+
commentInput.style.cssText = `
|
|
1618
|
+
width: 100% !important;
|
|
1619
|
+
height: 80px !important;
|
|
1620
|
+
padding: 12px !important;
|
|
1621
|
+
border: 1px solid #e0e0e0 !important;
|
|
1622
|
+
border-radius: 4px !important;
|
|
1623
|
+
background: #fafafa !important;
|
|
1624
|
+
color: #212121 !important;
|
|
1625
|
+
font-size: 14px !important;
|
|
1626
|
+
resize: none !important;
|
|
1627
|
+
margin-bottom: 24px !important;
|
|
1628
|
+
box-sizing: border-box !important;
|
|
1629
|
+
outline: none !important;
|
|
1630
|
+
opacity: 1 !important;
|
|
1631
|
+
`;
|
|
1632
|
+
// Button container
|
|
1633
|
+
const buttonContainer = doc.createElement('div');
|
|
1634
|
+
buttonContainer.style.cssText = `
|
|
1635
|
+
display: flex !important;
|
|
1636
|
+
gap: 8px !important;
|
|
1637
|
+
justify-content: flex-end !important;
|
|
1638
|
+
opacity: 1 !important;
|
|
1639
|
+
`;
|
|
1640
|
+
// No button
|
|
1641
|
+
const noBtn = doc.createElement('button');
|
|
1642
|
+
noBtn.id = '__cdp-verify-no';
|
|
1643
|
+
noBtn.textContent = 'Not Fixed';
|
|
1644
|
+
noBtn.style.cssText = `
|
|
1645
|
+
min-width: 120px !important;
|
|
1646
|
+
height: 36px !important;
|
|
1647
|
+
padding: 0 16px !important;
|
|
1648
|
+
border-radius: 4px !important;
|
|
1649
|
+
border: none !important;
|
|
1650
|
+
font-size: 14px !important;
|
|
1651
|
+
font-weight: 500 !important;
|
|
1652
|
+
cursor: pointer !important;
|
|
1653
|
+
text-transform: uppercase !important;
|
|
1654
|
+
letter-spacing: 0.5px !important;
|
|
1655
|
+
background: #f5f5f5 !important;
|
|
1656
|
+
color: #616161 !important;
|
|
1657
|
+
opacity: 1 !important;
|
|
1658
|
+
`;
|
|
1659
|
+
// Yes button
|
|
1660
|
+
const yesBtn = doc.createElement('button');
|
|
1661
|
+
yesBtn.id = '__cdp-verify-yes';
|
|
1662
|
+
yesBtn.textContent = 'Fixed';
|
|
1663
|
+
yesBtn.style.cssText = `
|
|
1664
|
+
min-width: 120px !important;
|
|
1665
|
+
height: 36px !important;
|
|
1666
|
+
padding: 0 16px !important;
|
|
1667
|
+
border-radius: 4px !important;
|
|
1668
|
+
border: none !important;
|
|
1669
|
+
font-size: 14px !important;
|
|
1670
|
+
font-weight: 500 !important;
|
|
1671
|
+
cursor: pointer !important;
|
|
1672
|
+
text-transform: uppercase !important;
|
|
1673
|
+
letter-spacing: 0.5px !important;
|
|
1674
|
+
background: #1976d2 !important;
|
|
1675
|
+
color: #ffffff !important;
|
|
1676
|
+
opacity: 1 !important;
|
|
1677
|
+
`;
|
|
1678
|
+
buttonContainer.appendChild(noBtn);
|
|
1679
|
+
buttonContainer.appendChild(yesBtn);
|
|
1680
|
+
card.appendChild(header);
|
|
1681
|
+
card.appendChild(questionBox);
|
|
1682
|
+
card.appendChild(commentInput);
|
|
1683
|
+
card.appendChild(buttonContainer);
|
|
1684
|
+
backdrop.appendChild(card);
|
|
1685
|
+
overlay.appendChild(backdrop);
|
|
1686
|
+
doc.body.appendChild(overlay);
|
|
1687
|
+
// Block keyboard events from reaching the app
|
|
1688
|
+
const blockKeyboard = (e) => {
|
|
1689
|
+
e.stopPropagation();
|
|
1690
|
+
};
|
|
1691
|
+
doc.addEventListener('keydown', blockKeyboard, true);
|
|
1692
|
+
doc.addEventListener('keyup', blockKeyboard, true);
|
|
1693
|
+
doc.addEventListener('keypress', blockKeyboard, true);
|
|
1694
|
+
const cleanup = () => {
|
|
1695
|
+
doc.removeEventListener('keydown', blockKeyboard, true);
|
|
1696
|
+
doc.removeEventListener('keyup', blockKeyboard, true);
|
|
1697
|
+
doc.removeEventListener('keypress', blockKeyboard, true);
|
|
1698
|
+
overlay.remove();
|
|
1699
|
+
};
|
|
1700
|
+
yesBtn?.addEventListener('click', () => {
|
|
1701
|
+
const comment = commentInput?.value?.trim() || undefined;
|
|
1702
|
+
cleanup();
|
|
1703
|
+
resolve({ resolved: true, comment });
|
|
1704
|
+
});
|
|
1705
|
+
noBtn?.addEventListener('click', () => {
|
|
1706
|
+
const comment = commentInput?.value?.trim() || undefined;
|
|
1707
|
+
cleanup();
|
|
1708
|
+
resolve({ resolved: false, comment });
|
|
1709
|
+
});
|
|
1710
|
+
});
|
|
1711
|
+
}, { typeLabel, issueTitle, issueId, questionText });
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Show a "Ready to begin?" overlay before starting a test replay
|
|
1715
|
+
* Uses DOM methods instead of innerHTML to support Trusted Types policies
|
|
1716
|
+
* Returns: 'cancel', 'begin', or 'rerecord'
|
|
1717
|
+
*/
|
|
1718
|
+
export async function showTestReadyOverlay(page, issueType, issueTitle, issueId, hasSequence = true) {
|
|
1719
|
+
const typeLabel = issueType === 'bug' ? 'Bug' : 'Feature';
|
|
1720
|
+
return await page.evaluate((params) => {
|
|
1721
|
+
return new Promise((resolve) => {
|
|
1722
|
+
const doc = globalThis.document;
|
|
1723
|
+
// Remove any existing overlay
|
|
1724
|
+
const existing = doc.getElementById('__cdp-test-ready-overlay');
|
|
1725
|
+
if (existing)
|
|
1726
|
+
existing.remove();
|
|
1727
|
+
// Create elements using DOM methods (Trusted Types compatible)
|
|
1728
|
+
const overlay = doc.createElement('div');
|
|
1729
|
+
overlay.id = '__cdp-test-ready-overlay';
|
|
1730
|
+
overlay.style.cssText = 'opacity: 1 !important;';
|
|
1731
|
+
const backdrop = doc.createElement('div');
|
|
1732
|
+
backdrop.style.cssText = `
|
|
1733
|
+
position: fixed !important;
|
|
1734
|
+
top: 0 !important;
|
|
1735
|
+
left: 0 !important;
|
|
1736
|
+
right: 0 !important;
|
|
1737
|
+
bottom: 0 !important;
|
|
1738
|
+
background: #424242 !important;
|
|
1739
|
+
z-index: 2147483646 !important;
|
|
1740
|
+
display: flex !important;
|
|
1741
|
+
align-items: center !important;
|
|
1742
|
+
justify-content: center !important;
|
|
1743
|
+
font-family: Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
|
|
1744
|
+
opacity: 1 !important;
|
|
1745
|
+
`;
|
|
1746
|
+
const card = doc.createElement('div');
|
|
1747
|
+
card.style.cssText = `
|
|
1748
|
+
background: #ffffff !important;
|
|
1749
|
+
border-radius: 4px !important;
|
|
1750
|
+
padding: 24px !important;
|
|
1751
|
+
max-width: 480px !important;
|
|
1752
|
+
width: 90% !important;
|
|
1753
|
+
box-shadow: 0 11px 15px -7px rgba(0,0,0,0.2), 0 24px 38px 3px rgba(0,0,0,0.14), 0 9px 46px 8px rgba(0,0,0,0.12) !important;
|
|
1754
|
+
color: #212121 !important;
|
|
1755
|
+
text-align: center !important;
|
|
1756
|
+
opacity: 1 !important;
|
|
1757
|
+
`;
|
|
1758
|
+
const testingLabel = doc.createElement('div');
|
|
1759
|
+
testingLabel.textContent = `Testing ${params.typeLabel} #${params.issueId}`;
|
|
1760
|
+
testingLabel.style.cssText = `
|
|
1761
|
+
font-size: 12px !important;
|
|
1762
|
+
color: #757575 !important;
|
|
1763
|
+
text-transform: uppercase !important;
|
|
1764
|
+
letter-spacing: 0.5px !important;
|
|
1765
|
+
margin-bottom: 16px !important;
|
|
1766
|
+
opacity: 1 !important;
|
|
1767
|
+
`;
|
|
1768
|
+
const description = doc.createElement('div');
|
|
1769
|
+
description.textContent = params.issueTitle;
|
|
1770
|
+
description.style.cssText = `
|
|
1771
|
+
font-size: 18px !important;
|
|
1772
|
+
font-weight: 500 !important;
|
|
1773
|
+
margin-bottom: 20px !important;
|
|
1774
|
+
padding: 16px !important;
|
|
1775
|
+
background: #f5f5f5 !important;
|
|
1776
|
+
border-radius: 4px !important;
|
|
1777
|
+
color: #212121 !important;
|
|
1778
|
+
text-align: center !important;
|
|
1779
|
+
opacity: 1 !important;
|
|
1780
|
+
`;
|
|
1781
|
+
const instructions = doc.createElement('div');
|
|
1782
|
+
instructions.textContent = params.hasSequence
|
|
1783
|
+
? 'The recorded sequence will replay. Watch for the issue.'
|
|
1784
|
+
: 'Recording will start. Reproduce the issue to verify.';
|
|
1785
|
+
instructions.style.cssText = `
|
|
1786
|
+
font-size: 14px !important;
|
|
1787
|
+
color: #616161 !important;
|
|
1788
|
+
margin-bottom: 24px !important;
|
|
1789
|
+
opacity: 1 !important;
|
|
1790
|
+
`;
|
|
1791
|
+
// Button container
|
|
1792
|
+
const buttonContainer = doc.createElement('div');
|
|
1793
|
+
buttonContainer.style.cssText = `
|
|
1794
|
+
display: flex !important;
|
|
1795
|
+
gap: 8px !important;
|
|
1796
|
+
justify-content: center !important;
|
|
1797
|
+
opacity: 1 !important;
|
|
1798
|
+
`;
|
|
1799
|
+
const buttonBaseCss = `
|
|
1800
|
+
min-width: 100px !important;
|
|
1801
|
+
height: 36px !important;
|
|
1802
|
+
padding: 0 16px !important;
|
|
1803
|
+
border-radius: 4px !important;
|
|
1804
|
+
border: none !important;
|
|
1805
|
+
font-size: 14px !important;
|
|
1806
|
+
font-weight: 500 !important;
|
|
1807
|
+
cursor: pointer !important;
|
|
1808
|
+
text-transform: uppercase !important;
|
|
1809
|
+
letter-spacing: 0.5px !important;
|
|
1810
|
+
opacity: 1 !important;
|
|
1811
|
+
`;
|
|
1812
|
+
const cancelBtn = doc.createElement('button');
|
|
1813
|
+
cancelBtn.textContent = 'CANCEL';
|
|
1814
|
+
cancelBtn.style.cssText = buttonBaseCss + 'background: #f5f5f5 !important; color: #616161 !important;';
|
|
1815
|
+
// Only show re-record button if there's already a sequence
|
|
1816
|
+
let rerecordBtn = null;
|
|
1817
|
+
if (params.hasSequence) {
|
|
1818
|
+
rerecordBtn = doc.createElement('button');
|
|
1819
|
+
rerecordBtn.textContent = 'RE-RECORD';
|
|
1820
|
+
rerecordBtn.style.cssText = buttonBaseCss + 'background: #ff9800 !important; color: #ffffff !important;';
|
|
1821
|
+
}
|
|
1822
|
+
const beginBtn = doc.createElement('button');
|
|
1823
|
+
beginBtn.textContent = 'BEGIN TEST';
|
|
1824
|
+
beginBtn.id = '__cdp-test-begin';
|
|
1825
|
+
beginBtn.style.cssText = buttonBaseCss + 'background: #1976d2 !important; color: #ffffff !important;';
|
|
1826
|
+
buttonContainer.appendChild(cancelBtn);
|
|
1827
|
+
if (rerecordBtn)
|
|
1828
|
+
buttonContainer.appendChild(rerecordBtn);
|
|
1829
|
+
buttonContainer.appendChild(beginBtn);
|
|
1830
|
+
card.appendChild(testingLabel);
|
|
1831
|
+
card.appendChild(description);
|
|
1832
|
+
card.appendChild(instructions);
|
|
1833
|
+
card.appendChild(buttonContainer);
|
|
1834
|
+
backdrop.appendChild(card);
|
|
1835
|
+
overlay.appendChild(backdrop);
|
|
1836
|
+
doc.body.appendChild(overlay);
|
|
1837
|
+
// Block keyboard events from reaching the app
|
|
1838
|
+
const blockKeyboard = (e) => {
|
|
1839
|
+
e.stopPropagation();
|
|
1840
|
+
};
|
|
1841
|
+
doc.addEventListener('keydown', blockKeyboard, true);
|
|
1842
|
+
doc.addEventListener('keyup', blockKeyboard, true);
|
|
1843
|
+
doc.addEventListener('keypress', blockKeyboard, true);
|
|
1844
|
+
const cleanup = () => {
|
|
1845
|
+
doc.removeEventListener('keydown', blockKeyboard, true);
|
|
1846
|
+
doc.removeEventListener('keyup', blockKeyboard, true);
|
|
1847
|
+
doc.removeEventListener('keypress', blockKeyboard, true);
|
|
1848
|
+
overlay.remove();
|
|
1849
|
+
};
|
|
1850
|
+
cancelBtn.addEventListener('click', () => {
|
|
1851
|
+
cleanup();
|
|
1852
|
+
resolve('cancel');
|
|
1853
|
+
});
|
|
1854
|
+
if (rerecordBtn) {
|
|
1855
|
+
rerecordBtn.addEventListener('click', () => {
|
|
1856
|
+
cleanup();
|
|
1857
|
+
resolve('rerecord');
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
beginBtn.addEventListener('click', () => {
|
|
1861
|
+
cleanup();
|
|
1862
|
+
resolve('begin');
|
|
1863
|
+
});
|
|
1864
|
+
});
|
|
1865
|
+
}, { typeLabel, issueTitle, issueId, hasSequence });
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Show a "Replay in progress" overlay that blocks user interaction
|
|
1869
|
+
* Returns a cleanup function to remove the overlay
|
|
1870
|
+
*/
|
|
1871
|
+
export async function showReplayOverlay(page, issueType, issueTitle, issueId) {
|
|
1872
|
+
const typeLabel = issueType === 'bug' ? 'Bug' : 'Feature';
|
|
1873
|
+
await page.evaluate((params) => {
|
|
1874
|
+
const doc = globalThis.document;
|
|
1875
|
+
// Create overlay - transparent with banner at top
|
|
1876
|
+
// pointerEvents: 'none' allows automated clicks to pass through while
|
|
1877
|
+
// event listeners still capture and block manual user interactions
|
|
1878
|
+
const overlay = doc.createElement('div');
|
|
1879
|
+
overlay.id = '__cdp-replay-overlay';
|
|
1880
|
+
Object.assign(overlay.style, {
|
|
1881
|
+
position: 'fixed',
|
|
1882
|
+
top: '0',
|
|
1883
|
+
left: '0',
|
|
1884
|
+
width: '100%',
|
|
1885
|
+
height: '100%',
|
|
1886
|
+
background: 'transparent',
|
|
1887
|
+
zIndex: '2147483647',
|
|
1888
|
+
pointerEvents: 'none',
|
|
1889
|
+
fontFamily: '"Roboto", -apple-system, system-ui, sans-serif'
|
|
1890
|
+
});
|
|
1891
|
+
// Banner at top (not centered)
|
|
1892
|
+
const card = doc.createElement('div');
|
|
1893
|
+
Object.assign(card.style, {
|
|
1894
|
+
position: 'absolute',
|
|
1895
|
+
top: '16px',
|
|
1896
|
+
left: '50%',
|
|
1897
|
+
transform: 'translateX(-50%)',
|
|
1898
|
+
background: 'rgba(33, 33, 33, 0.9)',
|
|
1899
|
+
borderRadius: '8px',
|
|
1900
|
+
padding: '12px 24px',
|
|
1901
|
+
maxWidth: '400px',
|
|
1902
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
|
|
1903
|
+
textAlign: 'center',
|
|
1904
|
+
display: 'flex',
|
|
1905
|
+
alignItems: 'center',
|
|
1906
|
+
gap: '12px'
|
|
1907
|
+
});
|
|
1908
|
+
// Spinner (small, inline)
|
|
1909
|
+
const spinner = doc.createElement('div');
|
|
1910
|
+
Object.assign(spinner.style, {
|
|
1911
|
+
width: '20px',
|
|
1912
|
+
height: '20px',
|
|
1913
|
+
border: '2px solid rgba(255,255,255,0.3)',
|
|
1914
|
+
borderTop: '2px solid #4fc3f7',
|
|
1915
|
+
borderRadius: '50%',
|
|
1916
|
+
animation: 'cdp-spin 1s linear infinite',
|
|
1917
|
+
flexShrink: '0'
|
|
1918
|
+
});
|
|
1919
|
+
// Add keyframe animations (spin for spinner, shake for feedback)
|
|
1920
|
+
const style = doc.createElement('style');
|
|
1921
|
+
style.textContent = `
|
|
1922
|
+
@keyframes cdp-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
1923
|
+
@keyframes cdp-shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-4px); } 40% { transform: translateX(4px); } 60% { transform: translateX(-4px); } 80% { transform: translateX(4px); } }
|
|
1924
|
+
`;
|
|
1925
|
+
doc.head.appendChild(style);
|
|
1926
|
+
globalThis.__cdpReplayStyle = style;
|
|
1927
|
+
// Instructions/status text
|
|
1928
|
+
const instructions = doc.createElement('div');
|
|
1929
|
+
instructions.textContent = `Replaying ${params.typeLabel} #${params.issueId}...`;
|
|
1930
|
+
Object.assign(instructions.style, {
|
|
1931
|
+
fontSize: '14px',
|
|
1932
|
+
color: '#ffffff',
|
|
1933
|
+
whiteSpace: 'nowrap'
|
|
1934
|
+
});
|
|
1935
|
+
card.appendChild(spinner);
|
|
1936
|
+
card.appendChild(instructions);
|
|
1937
|
+
overlay.appendChild(card);
|
|
1938
|
+
doc.body.appendChild(overlay);
|
|
1939
|
+
// Block all user interactions with escalating feedback
|
|
1940
|
+
let attemptCount = 0;
|
|
1941
|
+
const attitudeMessages = [
|
|
1942
|
+
"I said WAIT.",
|
|
1943
|
+
"Seriously?",
|
|
1944
|
+
"The spinner means BUSY.",
|
|
1945
|
+
"Do you want bugs? Because this is how you get bugs.",
|
|
1946
|
+
"...",
|
|
1947
|
+
];
|
|
1948
|
+
// Events that trigger shake/attitude (intentional user actions)
|
|
1949
|
+
const feedbackEvents = ['click', 'keydown', 'touchend'];
|
|
1950
|
+
// Events to block silently (prevent default but no feedback)
|
|
1951
|
+
const silentBlockEvents = ['mousedown', 'mouseup', 'keyup', 'keypress', 'touchstart'];
|
|
1952
|
+
const blockWithFeedback = (e) => {
|
|
1953
|
+
// Don't block if CDP replay is in progress (flag set by replay executor)
|
|
1954
|
+
if (globalThis.__cdpReplayClickInProgress)
|
|
1955
|
+
return;
|
|
1956
|
+
e.preventDefault();
|
|
1957
|
+
e.stopPropagation();
|
|
1958
|
+
attemptCount++;
|
|
1959
|
+
console.log('[cdp-tools] Blocked user interaction during replay:', e.type, 'target:', e.target?.tagName);
|
|
1960
|
+
// Shake the instructions text
|
|
1961
|
+
instructions.style.animation = 'none';
|
|
1962
|
+
void instructions.offsetWidth; // Force reflow
|
|
1963
|
+
instructions.style.animation = 'cdp-shake 0.3s ease';
|
|
1964
|
+
// After 3 attempts, show attitude
|
|
1965
|
+
if (attemptCount > 3) {
|
|
1966
|
+
const msgIndex = Math.min(attemptCount - 4, attitudeMessages.length - 1);
|
|
1967
|
+
instructions.textContent = attitudeMessages[msgIndex];
|
|
1968
|
+
instructions.style.color = '#d32f2f';
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
const blockSilently = (e) => {
|
|
1972
|
+
// Don't block if CDP replay is in progress (flag set by replay executor)
|
|
1973
|
+
if (globalThis.__cdpReplayClickInProgress)
|
|
1974
|
+
return;
|
|
1975
|
+
e.preventDefault();
|
|
1976
|
+
e.stopPropagation();
|
|
1977
|
+
};
|
|
1978
|
+
feedbackEvents.forEach(evt => doc.addEventListener(evt, blockWithFeedback, true));
|
|
1979
|
+
silentBlockEvents.forEach(evt => doc.addEventListener(evt, blockSilently, true));
|
|
1980
|
+
globalThis.__cdpReplayBlocker = {
|
|
1981
|
+
blockWithFeedback,
|
|
1982
|
+
blockSilently,
|
|
1983
|
+
feedbackEvents,
|
|
1984
|
+
silentBlockEvents
|
|
1985
|
+
};
|
|
1986
|
+
}, { typeLabel, issueTitle, issueId });
|
|
1987
|
+
// Return cleanup function
|
|
1988
|
+
return async () => {
|
|
1989
|
+
await page.evaluate(() => {
|
|
1990
|
+
const doc = globalThis.document;
|
|
1991
|
+
const overlay = doc.getElementById('__cdp-replay-overlay');
|
|
1992
|
+
if (overlay)
|
|
1993
|
+
overlay.remove();
|
|
1994
|
+
const style = globalThis.__cdpReplayStyle;
|
|
1995
|
+
if (style)
|
|
1996
|
+
style.remove();
|
|
1997
|
+
const blocker = globalThis.__cdpReplayBlocker;
|
|
1998
|
+
if (blocker) {
|
|
1999
|
+
blocker.feedbackEvents.forEach((evt) => doc.removeEventListener(evt, blocker.blockWithFeedback, true));
|
|
2000
|
+
blocker.silentBlockEvents.forEach((evt) => doc.removeEventListener(evt, blocker.blockSilently, true));
|
|
2001
|
+
delete globalThis.__cdpReplayBlocker;
|
|
2002
|
+
}
|
|
2003
|
+
delete globalThis.__cdpReplayStyle;
|
|
2004
|
+
}).catch(() => { });
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
//# sourceMappingURL=interaction-recorder.js.map
|