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
package/docs/replay.md
ADDED
|
@@ -0,0 +1,1411 @@
|
|
|
1
|
+
# Command Replay
|
|
2
|
+
|
|
3
|
+
Record and replay command sequences for testing, automation, and debugging workflows.
|
|
4
|
+
|
|
5
|
+
> **Tip:** Use the `replay-agent` (`.claude/agents/replay-agent.md`) to build sequences through investigation - it records your tool calls automatically.
|
|
6
|
+
>
|
|
7
|
+
> For a condensed, agent-facing version of this material see
|
|
8
|
+
> `skills/cdp-tools/references/sequences.md`. This document is the fuller
|
|
9
|
+
> reference with worked examples.
|
|
10
|
+
|
|
11
|
+
## Actions
|
|
12
|
+
|
|
13
|
+
Every capability below is the one `replay` tool, dispatched on `action`:
|
|
14
|
+
|
|
15
|
+
| Group | Actions |
|
|
16
|
+
|---|---|
|
|
17
|
+
| History | `history`, `repeat`, `runFromLog` |
|
|
18
|
+
| Authoring | `create`, `recordInteraction`, `insert`, `addConditional`, `declare` |
|
|
19
|
+
| Managing | `list`, `get`, `delete`, `export`, `load`, `listSaved`, `deleteSaved` |
|
|
20
|
+
| Running | `run`, `runAll`, `step`, `finish`, `status`, `cancel` |
|
|
21
|
+
|
|
22
|
+
There is no `stopInteraction` and no `save` action - see
|
|
23
|
+
[Recording Interactions](#recording-interactions) and
|
|
24
|
+
[Saving and Loading](#saving-and-loading).
|
|
25
|
+
|
|
26
|
+
## Recording Interactions
|
|
27
|
+
|
|
28
|
+
The easiest way to create a sequence is to record your interactions directly in
|
|
29
|
+
the browser.
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
// Launch Chrome with a meaningful name
|
|
33
|
+
launchChrome({ reference: "my-signup-test" })
|
|
34
|
+
|
|
35
|
+
// Start recording - THIS CALL BLOCKS until you finish in the browser
|
|
36
|
+
replay({ action: 'recordInteraction', connectionReason: 'my-signup-test' })
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**`recordInteraction` blocks until the person finishes in the browser overlay.**
|
|
40
|
+
There is no separate stop call. The tool call returns only once you click ✓
|
|
41
|
+
(complete) or ✕ (cancel) in the overlay, and its response *is* the created
|
|
42
|
+
sequence summary. Because it waits on a human, don't call it unattended.
|
|
43
|
+
|
|
44
|
+
If no connection exists for `connectionReason`, Chrome is auto-launched - but
|
|
45
|
+
only if you also pass `startUrl` (or an `issueId` whose issue carries one). If a
|
|
46
|
+
connection already exists and you pass `startUrl`, the page navigates there
|
|
47
|
+
first.
|
|
48
|
+
|
|
49
|
+
### The Recording Overlay
|
|
50
|
+
|
|
51
|
+
A visual overlay appears showing:
|
|
52
|
+
- Recording status (`REC` / `PAUSED`)
|
|
53
|
+
- Event count and duration
|
|
54
|
+
- Coordinates and element info
|
|
55
|
+
- Buttons: 💬 Comment | ⏸ Pause | ↺ Reset | ✓ Complete | ✕ Cancel
|
|
56
|
+
|
|
57
|
+
Pass `showOverlay: false` to suppress it.
|
|
58
|
+
|
|
59
|
+
### Add Comments
|
|
60
|
+
|
|
61
|
+
During recording, add comments to document expected behavior:
|
|
62
|
+
- Click the 💬 button in the overlay
|
|
63
|
+
- Or press **Ctrl/Cmd+Shift+C** (plain comment), **Ctrl/Cmd+Shift+B** (bug),
|
|
64
|
+
**Ctrl/Cmd+Shift+F** (feature)
|
|
65
|
+
|
|
66
|
+
Comments are attached to the previous action and appear in exported tests.
|
|
67
|
+
|
|
68
|
+
**Bug and feature comments create issues.** Each `bug`/`feature` comment becomes
|
|
69
|
+
an entry in the issue tracker with its own copy of the sequence saved as
|
|
70
|
+
`<type>-<id>-repro`. When a recording contains any such comment, no plain
|
|
71
|
+
in-memory sequence is created - the issue sequences are the output.
|
|
72
|
+
|
|
73
|
+
### Naming
|
|
74
|
+
|
|
75
|
+
The sequence is named, in order of preference:
|
|
76
|
+
1. the `name` you passed,
|
|
77
|
+
2. `<issueType>-<issueId>-repro` when you passed `issueId`,
|
|
78
|
+
3. the `connectionReason`.
|
|
79
|
+
|
|
80
|
+
If a sequence with that name already exists you get a conflict response. Re-run
|
|
81
|
+
with a different `name`, or with `overwrite: true`.
|
|
82
|
+
|
|
83
|
+
### Recording Options
|
|
84
|
+
|
|
85
|
+
The conversion from raw input events to sequence commands is tunable. All four
|
|
86
|
+
flags default to today's behaviour, so omitting them changes nothing:
|
|
87
|
+
|
|
88
|
+
| Option | Default | Effect |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `simplifyEvents` | `true` | Collapse noisy raw events (mousemove runs, key repeats) before conversion. `false` keeps them all. |
|
|
91
|
+
| `includeHovers` | `false` | Emit `input({ action: 'mousemove' })` steps for hovers. Needs `simplifyEvents: false` to keep more than the settled positions. |
|
|
92
|
+
| `preferCoordinates` | `false` | Emit `x,y` clicks even where a selector was captured. Use for canvas/3D/drag-heavy UIs. |
|
|
93
|
+
| `preferSelectors` | `false` | Emit selector clicks wherever a selector exists - *including* canvas elements, which otherwise fall back to coordinates. |
|
|
94
|
+
|
|
95
|
+
If both `preferCoordinates` and `preferSelectors` are `true`, **`preferSelectors`
|
|
96
|
+
wins** - the more portable of the two is chosen.
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
replay({
|
|
100
|
+
action: 'recordInteraction',
|
|
101
|
+
connectionReason: 'canvas-bug',
|
|
102
|
+
preferCoordinates: true, // a WebGL canvas has no useful selectors
|
|
103
|
+
includeHovers: true, // the bug is a hover artefact
|
|
104
|
+
simplifyEvents: false
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`recordInteraction` also accepts `outputFormat`, which appends a dump to the
|
|
109
|
+
usual recording summary:
|
|
110
|
+
|
|
111
|
+
- `events` - the raw captured input events as JSON (this is the only place they
|
|
112
|
+
are ever available; they are not stored with the sequence)
|
|
113
|
+
- `commands` - the converted command list as JSON
|
|
114
|
+
- `review` - a human-readable walkthrough of the captured events: one numbered
|
|
115
|
+
entry per interaction with its coordinates, the element and selector found for
|
|
116
|
+
it, plus navigations, pastes and the comments the person left while recording
|
|
117
|
+
- `playwright` / `puppeteer` - generated test code for the fresh recording
|
|
118
|
+
|
|
119
|
+
Use `outputFormat: 'events'` when a recording produced surprising commands and
|
|
120
|
+
you need to see what the recorder actually captured; `review` is the same
|
|
121
|
+
information in a form you can read, and is the better choice when you want to
|
|
122
|
+
decide whether a step should use a selector or coordinates.
|
|
123
|
+
|
|
124
|
+
### Recording Against an Issue
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
replay({ action: 'recordInteraction', connectionReason: 'bug-7', issueId: 7 })
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The issue's `type`, `title` and `startUrl` are used automatically, a fullscreen
|
|
131
|
+
issue overlay is shown, and the finished sequence is saved into the issues
|
|
132
|
+
folder and linked to issue #7.
|
|
133
|
+
|
|
134
|
+
## Exporting Tests
|
|
135
|
+
|
|
136
|
+
Export sequences as Playwright or Puppeteer tests:
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
// Export as Playwright test
|
|
140
|
+
replay({ action: 'export', name: 'my-signup-test', format: 'playwright' })
|
|
141
|
+
// Creates: tests/e2e/my-signup-test.spec.ts
|
|
142
|
+
// Also saves: .cdp-tools/sequences/my-signup-test.json
|
|
143
|
+
|
|
144
|
+
// Export as Puppeteer test
|
|
145
|
+
replay({ action: 'export', name: 'my-signup-test', format: 'puppeteer' })
|
|
146
|
+
// Creates: tests/puppeteer/my-signup-test.test.js
|
|
147
|
+
|
|
148
|
+
// Export sequence JSON only (default format)
|
|
149
|
+
replay({ action: 'export', name: 'my-signup-test', format: 'sequence' })
|
|
150
|
+
// Creates: .cdp-tools/sequences/my-signup-test.json
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`export` always writes the sequence JSON first, whatever the format. If a target
|
|
154
|
+
file already exists you get a conflict response; re-run with `overwrite: true`.
|
|
155
|
+
|
|
156
|
+
Only `navigate` and `input` steps are translated into Playwright/Puppeteer code
|
|
157
|
+
- debugging steps (`breakpoint`, `inspect`, `request`, ...) have no equivalent
|
|
158
|
+
and are dropped from the generated test.
|
|
159
|
+
|
|
160
|
+
### Configure Export Paths
|
|
161
|
+
|
|
162
|
+
In `.cdp-tools/config.json`:
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"replay": {
|
|
167
|
+
"playwrightExportPath": "./tests/e2e",
|
|
168
|
+
"puppeteerExportPath": "./tests/puppeteer"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Preview Before Export
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
// Preview as Playwright code
|
|
177
|
+
replay({ action: 'get', name: 'my-signup-test', outputFormat: 'playwright' })
|
|
178
|
+
|
|
179
|
+
// Preview as Puppeteer code
|
|
180
|
+
replay({ action: 'get', name: 'my-signup-test', outputFormat: 'puppeteer' })
|
|
181
|
+
|
|
182
|
+
// The raw command list as JSON (what actually gets executed)
|
|
183
|
+
replay({ action: 'get', name: 'my-signup-test', outputFormat: 'commands' })
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`outputFormat` on `get` accepts `commands`, `playwright` and `puppeteer`.
|
|
187
|
+
`events` and `review` are not valid here and each returns an error explaining
|
|
188
|
+
why: a stored sequence holds converted *commands*, never the raw input events,
|
|
189
|
+
and both of those formats render events. The raw events exist only during a
|
|
190
|
+
recording - see [Recording Options](#recording-options) for
|
|
191
|
+
`outputFormat: 'events'` and `outputFormat: 'review'` on `recordInteraction`.
|
|
192
|
+
|
|
193
|
+
## Visual Replay Cursor
|
|
194
|
+
|
|
195
|
+
When replaying sequences, a visual cursor shows where clicks happen:
|
|
196
|
+
|
|
197
|
+
- **Animated cursor** moves to the click position before clicking
|
|
198
|
+
- **Green ripple** on the click
|
|
199
|
+
- **Key press toast** shows keyboard input
|
|
200
|
+
|
|
201
|
+
The cursor is only driven for *coordinate* clicks (`input({ action: 'click', x,
|
|
202
|
+
y })`) and for `input({ action: 'press' })`. Selector-based clicks execute
|
|
203
|
+
without a cursor effect.
|
|
204
|
+
|
|
205
|
+
Configure in `.cdp-tools/config.json`:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"replay": {
|
|
210
|
+
"showCursor": true
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Creating Sequences from History
|
|
216
|
+
|
|
217
|
+
### From Command History
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
// View command history
|
|
221
|
+
replay({ action: 'history', limit: 20 })
|
|
222
|
+
|
|
223
|
+
// Create sequence from history indices
|
|
224
|
+
replay({
|
|
225
|
+
action: 'create',
|
|
226
|
+
name: 'login-flow',
|
|
227
|
+
indices: [1, 2, 3, 4, 5]
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Every tool response footer shows its own history index, so you usually don't
|
|
232
|
+
need to call `history` first.
|
|
233
|
+
|
|
234
|
+
### With Metadata
|
|
235
|
+
|
|
236
|
+
Add description, expected outcome and a start URL for better documentation:
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
replay({
|
|
240
|
+
action: 'create',
|
|
241
|
+
name: 'login-flow',
|
|
242
|
+
description: 'Logs into the application with test credentials',
|
|
243
|
+
expectedOutcome: 'User should be redirected to dashboard with welcome message',
|
|
244
|
+
startUrl: 'http://localhost:3000/login',
|
|
245
|
+
indices: [1, 2, 3, 4, 5]
|
|
246
|
+
})
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
These fields are saved to disk and displayed when listing sequences.
|
|
250
|
+
|
|
251
|
+
### Re-running History Directly
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
// Execute commands straight from history without making a sequence
|
|
255
|
+
replay({ action: 'repeat', indices: [12, 13] })
|
|
256
|
+
|
|
257
|
+
// Execute lines from .cdp-tools/history.log (1-indexed, line 1 = most recent)
|
|
258
|
+
replay({ action: 'runFromLog', lines: [3, 4, 5] })
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Both stop at the first failing command. Both infer `connectionReason` from a
|
|
262
|
+
`launchChrome`/`connectDebugger` command in the selection if you don't pass one,
|
|
263
|
+
and error out if the commands need a connection and none can be determined.
|
|
264
|
+
|
|
265
|
+
## Tool-Name Validation
|
|
266
|
+
|
|
267
|
+
`create` and `load` reject a sequence whose steps name a tool that doesn't
|
|
268
|
+
exist, **before anything runs**:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
Error: Sequence "login-flow" references 1 unknown tool name
|
|
272
|
+
The "load" action was rejected before any step ran, so no browser state was changed.
|
|
273
|
+
|
|
274
|
+
- Step 4: `navigatee` is not a known tool - did you mean `navigate`?
|
|
275
|
+
|
|
276
|
+
**Fix:** correct the `tool` field on the listed step(s).
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Notes:
|
|
280
|
+
- Only the tool *name* is checked. Params are deliberately not validated against
|
|
281
|
+
the tools' schemas, because a param may legitimately hold a `{{var:...}}` or
|
|
282
|
+
`{{timestamp}}` token at rest that only resolves to its real type at run time.
|
|
283
|
+
- `conditional` is exempt - it is a virtual step tool the executor handles
|
|
284
|
+
itself and it is never a registered tool (see [Conditional Steps](#conditional-steps)).
|
|
285
|
+
- A rejected `create` does not clobber an existing same-named sequence; a
|
|
286
|
+
rejected `load` is dropped from memory so it can't be run by id.
|
|
287
|
+
|
|
288
|
+
## Managing Sequences
|
|
289
|
+
|
|
290
|
+
```javascript
|
|
291
|
+
// List all in-memory sequences
|
|
292
|
+
replay({ action: 'list' })
|
|
293
|
+
|
|
294
|
+
// View sequence details (by sequenceId or name)
|
|
295
|
+
replay({ action: 'get', sequenceId: 'seq-1234567890' })
|
|
296
|
+
replay({ action: 'get', name: 'login-flow' }) // memory first, then disk (fuzzy name match)
|
|
297
|
+
|
|
298
|
+
// Delete a sequence from memory
|
|
299
|
+
replay({ action: 'delete', sequenceId: 'seq-1234567890' })
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Saving and Loading
|
|
303
|
+
|
|
304
|
+
```javascript
|
|
305
|
+
// Export sequence to disk (working directory)
|
|
306
|
+
replay({ action: 'export', sequenceId: 'seq-1234567890', format: 'sequence' })
|
|
307
|
+
// Saves to: .cdp-tools/sequences/<name>.json
|
|
308
|
+
|
|
309
|
+
// Export to global location (accessible from any directory)
|
|
310
|
+
replay({ action: 'export', sequenceId: 'seq-1234567890', format: 'sequence', global: true })
|
|
311
|
+
// Saves to: ~/.cdp-tools/sequences/<name>.json
|
|
312
|
+
|
|
313
|
+
// List saved sequences on disk (add showAll: true to include completed issues)
|
|
314
|
+
replay({ action: 'listSaved' })
|
|
315
|
+
|
|
316
|
+
// Load sequence from disk
|
|
317
|
+
replay({ action: 'load', filename: 'login-flow.json' })
|
|
318
|
+
|
|
319
|
+
// Load into history (for editing)
|
|
320
|
+
replay({ action: 'load', filename: 'login-flow.json', intoHistory: true })
|
|
321
|
+
|
|
322
|
+
// Delete saved file
|
|
323
|
+
replay({ action: 'deleteSaved', filename: 'login-flow.json' })
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`run` and `get` load by `name` from disk on their own, so `load` is only needed
|
|
327
|
+
when you want the sequence in memory (or in history) first.
|
|
328
|
+
|
|
329
|
+
### Editing a sequence mid-session
|
|
330
|
+
|
|
331
|
+
Edit the file and run it - the edit is what runs. The sequences directories are
|
|
332
|
+
watched the way a managed dev server watches its own sources, so an edited file
|
|
333
|
+
is re-read into memory shortly after you save it, and a run re-checks the file's
|
|
334
|
+
timestamp on the way past rather than waiting for the watcher. Both are needed:
|
|
335
|
+
the watcher keeps memory honest while you work, and the check at run time closes
|
|
336
|
+
the gap between saving a file and immediately running it.
|
|
337
|
+
|
|
338
|
+
In-memory copies used to shadow disk for the rest of the session - you edited a
|
|
339
|
+
sequence, ran it by name, and silently got the previous version, while `runAll`
|
|
340
|
+
reloaded the tree first and ran the new one. The same sequence behaved
|
|
341
|
+
differently depending on how it was invoked.
|
|
342
|
+
|
|
343
|
+
A file that is missing or mid-write leaves the loaded copy in place: a watcher
|
|
344
|
+
fires as readily during a write as after one, and dropping a good sequence
|
|
345
|
+
because it was caught half-saved is worse than the staleness this replaces. A
|
|
346
|
+
sequence built from history and never saved is untouched by any of this - there
|
|
347
|
+
is no file to reload it from.
|
|
348
|
+
|
|
349
|
+
## Running Sequences
|
|
350
|
+
|
|
351
|
+
### Basic Run
|
|
352
|
+
|
|
353
|
+
```javascript
|
|
354
|
+
replay({
|
|
355
|
+
action: 'run',
|
|
356
|
+
sequenceId: 'seq-1234567890',
|
|
357
|
+
connectionReason: 'test-session'
|
|
358
|
+
})
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Or by name, which also finds it on disk:
|
|
362
|
+
|
|
363
|
+
```javascript
|
|
364
|
+
replay({ action: 'run', name: 'login-flow', connectionReason: 'test-session' })
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Background by Default (breaking change in 0.7)
|
|
368
|
+
|
|
369
|
+
**`run` no longer blocks.** It validates the request (sequence exists, tool
|
|
370
|
+
names known, `startFrom` in range, variables supplied), registers a run, and
|
|
371
|
+
returns immediately with a run id:
|
|
372
|
+
|
|
373
|
+
```javascript
|
|
374
|
+
replay({ action: 'run', name: 'login-flow' })
|
|
375
|
+
// -> Run started in the background ... Run id: `run-3-mdkq1x2`
|
|
376
|
+
|
|
377
|
+
replay({ action: 'status', runId: 'run-3-mdkq1x2' }) // progress while running,
|
|
378
|
+
// the full result once settled
|
|
379
|
+
replay({ action: 'cancel', runId: 'run-3-mdkq1x2' }) // stop it
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
While a run executes, `status` with its `runId` reports the current step and
|
|
383
|
+
tool; once the run settles it returns exactly what the old blocking `run`
|
|
384
|
+
would have returned (step results, debug state, `killChromeOnFinish` outcome).
|
|
385
|
+
Post-run cleanup - cursor/overlay removal, debug state, `killChromeOnFinish` -
|
|
386
|
+
happens in the background before the run's status turns terminal, exactly once.
|
|
387
|
+
|
|
388
|
+
Run states: `running` → (`cancelling` →) one of `completed`, `failed`,
|
|
389
|
+
`cancelled`, or `paused` (stepTo / breakpoint / click-validation - drive it
|
|
390
|
+
with `step`/`finish` as before).
|
|
391
|
+
|
|
392
|
+
Lifetime and limits:
|
|
393
|
+
|
|
394
|
+
- Several runs can execute concurrently, including two runs of the same
|
|
395
|
+
sequence - the run id is what tells them apart.
|
|
396
|
+
- Settled runs and their results are kept **in memory for 30 minutes** (at
|
|
397
|
+
most 50 records). After that, or after a server restart (including the
|
|
398
|
+
supervisor's hot-restart on rebuild, which also kills any in-flight run),
|
|
399
|
+
`status`/`cancel` with that id return `REPLAY_RUN_NOT_FOUND`.
|
|
400
|
+
- `cancel` with a `runId` aborts the run's controller. The run's signal is
|
|
401
|
+
forwarded to every step's tool handler; cancellation also reaches nested
|
|
402
|
+
sequences (`conditional` flows, nested `replay run` steps) - they share the
|
|
403
|
+
parent run's signal. Status shows `cancelling` until the run actually stops.
|
|
404
|
+
What a cancel does to the step that is currently in flight depends on the
|
|
405
|
+
tool, and the three levels are genuinely different (see the table below).
|
|
406
|
+
|
|
407
|
+
**Which tools honour cancellation** (canonical list - `#110`):
|
|
408
|
+
|
|
409
|
+
| Tool | On cancel | What that means |
|
|
410
|
+
| --- | --- | --- |
|
|
411
|
+
| `wait` (all forms: `selector`, `selectorGone`, `expression`, `ms`) | **cancelled** | Polling/sleep stops mid-step. Nothing was in flight to abandon. |
|
|
412
|
+
| `request` (`destination: 'node'`) | **cancelled** | The external signal is composed into the fetch's controller, so the **socket is closed** - the server sees the request aborted. Its own `timeoutMs` is still reported as a timeout, separately. |
|
|
413
|
+
| `navigate` (`goto`, `reload`, `back`, `forward`) | **stops waiting** | Deliberate owner decision: `Page.stopLoading` is *not* called, because a half-loaded page that later steps act on is worse than a loaded one. The page finishes loading in the background; the step stops waiting for it. |
|
|
414
|
+
| `inspect` (`evaluateExpression`) | **stops waiting** | CDP cannot recall a `Runtime.evaluate`; the expression keeps running in the target. Other `inspect` actions read captured state and only get the entry checkpoint. |
|
|
415
|
+
| `content` (`parse`) | **stops waiting** | The plugin's `waitFor` predicate (default up to 8s) is abandoned. Extraction itself is one `page.evaluate`. |
|
|
416
|
+
| `input` (every action) | **checkpoint only** | Input events cannot be recalled: once `Input.dispatchMouseEvent` is on the wire, Chrome **will** process it. Cancellation prevents events that had *not* gone out yet - checkpoints sit after connection/selector resolution, immediately before each dispatch, and between events in multi-dispatch paths (Tab loops, drag stepping, clear-and-retype, pinch). A cancelled drag still releases the mouse button. **Nothing already dispatched is undone.** |
|
|
417
|
+
| `breakpoint({ action: 'await' })` | **checkpoint + fails the step** | Cancelling the wait removes a breakpoint the step created and fails the step. (Before #110 it reported `success: true` - a cancelled step recorded as passed.) |
|
|
418
|
+
| `request` (`destination: 'browser'`) | **checkpoint only** | The `fetch` runs inside the page and is unreachable from the server; it runs to its own timeout. |
|
|
419
|
+
| `screenshot`, `content` (other actions), `inspect` (other actions) | **checkpoint only** | Enough not to *start* a capture/read after the cancel; the capture itself is a single call with nothing to cancel. |
|
|
420
|
+
| `dom`, `network`, everything else | **next step boundary** | No real wait or loop to interrupt (`network` reads an in-memory buffer). The in-flight call finishes and the run stops before the next step. |
|
|
421
|
+
|
|
422
|
+
Two caveats that apply to every row: work already dispatched to the browser
|
|
423
|
+
may still take effect, and a "stops waiting" step leaves work running in the
|
|
424
|
+
target that no one is watching any more.
|
|
425
|
+
- A nested run started by a sequence step (a `conditional` flow, or a
|
|
426
|
+
`replay run` step - which is forced to `wait: true`) is part of its parent
|
|
427
|
+
run, never a separate top-level run.
|
|
428
|
+
|
|
429
|
+
**Migration:** pass `wait: true` to keep the pre-0.7 blocking behaviour:
|
|
430
|
+
|
|
431
|
+
```javascript
|
|
432
|
+
replay({ action: 'run', name: 'login-flow', wait: true }) // blocks, returns full result
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Auto-Launch Chrome
|
|
436
|
+
|
|
437
|
+
If the sequence starts with `launchChrome`, no `connectionReason` is needed -
|
|
438
|
+
the launch step's `reference` becomes the run's connection.
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
replay({ action: 'run', sequenceId: 'seq-my-flow' })
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Otherwise, if the sequence needs a browser and no connection is active, Chrome
|
|
445
|
+
is launched as a **fresh instance** using `connectionReason` (or a reference
|
|
446
|
+
derived from the sequence name when you didn't pass one).
|
|
447
|
+
|
|
448
|
+
### Retargeting a Run
|
|
449
|
+
|
|
450
|
+
```javascript
|
|
451
|
+
// Point a staging-recorded sequence at local
|
|
452
|
+
replay({ action: 'run', name: 'checkout', baseUrl: 'http://localhost:3000' })
|
|
453
|
+
|
|
454
|
+
// Enter through a freshly minted link, once
|
|
455
|
+
replay({ action: 'run', name: 'magic-link-login', startUrl: 'https://app.example.com/m/abc123' })
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
- **`baseUrl`** rewrites the origin of *every absolute* `http(s)` URL in the
|
|
459
|
+
sequence - the stored `startUrl` and any string param in any step (a
|
|
460
|
+
`navigate goto` url, a `request` url, ...) - keeping path, query and hash.
|
|
461
|
+
Relative URLs are untouched. The stored sequence is never mutated.
|
|
462
|
+
- **`startUrl`** replaces the sequence's start URL wholesale for this run,
|
|
463
|
+
applied after any rebasing.
|
|
464
|
+
- Neither is preserved across a mid-run pause and `step`/`finish` resume, which
|
|
465
|
+
re-reads the stored sequence.
|
|
466
|
+
|
|
467
|
+
### Timeout Configuration
|
|
468
|
+
|
|
469
|
+
```javascript
|
|
470
|
+
replay({
|
|
471
|
+
action: 'run',
|
|
472
|
+
sequenceId: 'seq-slow-flow',
|
|
473
|
+
connectionReason: 'test-session',
|
|
474
|
+
stepTimeout: 60000, // per step (default: 30000)
|
|
475
|
+
totalTimeout: 600000 // whole run (default: 300000)
|
|
476
|
+
})
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Each step's tool call is bounded by `min(stepTimeout, remaining totalTimeout)`.
|
|
480
|
+
A step that exceeds its bound fails the run at that step, like any other step
|
|
481
|
+
failure - the error names the step, the tool, and the limit that fired. The
|
|
482
|
+
run stops immediately; the timed-out tool call is not interrupted and may
|
|
483
|
+
still complete in the background (its side effects can still land).
|
|
484
|
+
|
|
485
|
+
Exceptions:
|
|
486
|
+
|
|
487
|
+
- `wait` steps are exempt from `stepTimeout` - a wait carries its own
|
|
488
|
+
`timeoutMs` bound (default 15000) and fails itself on expiry. It is still
|
|
489
|
+
capped by the remaining `totalTimeout`.
|
|
490
|
+
- Breakpoint pauses are unaffected: a step that hits a breakpoint returns
|
|
491
|
+
immediately with pause info, so an intentional pause never trips the
|
|
492
|
+
step timeout.
|
|
493
|
+
|
|
494
|
+
### Start From a Specific Step
|
|
495
|
+
|
|
496
|
+
```javascript
|
|
497
|
+
replay({
|
|
498
|
+
action: 'run',
|
|
499
|
+
name: 'login-flow',
|
|
500
|
+
connectionReason: 'test-session',
|
|
501
|
+
startFrom: 5 // Skip steps 1-4, start at step 5 (1-indexed)
|
|
502
|
+
})
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
`startFrom` beyond the sequence length is rejected.
|
|
506
|
+
|
|
507
|
+
### Pause, Step, Finish
|
|
508
|
+
|
|
509
|
+
```javascript
|
|
510
|
+
replay({ action: 'run', name: 'login-flow', stepTo: 3 }) // run steps 1-3, then pause
|
|
511
|
+
replay({ action: 'status' }) // where am I?
|
|
512
|
+
replay({ action: 'step', stepCount: 2 }) // run the next 2 steps
|
|
513
|
+
replay({ action: 'finish' }) // run the rest
|
|
514
|
+
replay({ action: 'cancel' }) // drop the paused session
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
A bare `cancel` prefers the paused session; with background runs in flight,
|
|
518
|
+
address the one you mean with `runId` (a bare `cancel` also works when exactly
|
|
519
|
+
one run is executing and nothing is paused). `status` without `runId` shows
|
|
520
|
+
the paused session plus all recent runs.
|
|
521
|
+
|
|
522
|
+
While paused you can run tools by hand and then splice them into the sequence:
|
|
523
|
+
|
|
524
|
+
```javascript
|
|
525
|
+
replay({ action: 'history' }) // required before inserting by index
|
|
526
|
+
replay({ action: 'insert' }) // show what's insertable
|
|
527
|
+
replay({ action: 'insert', insertIndices: [42, 43], insertAfterStep: 3 })
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
By default `insert` creates a new sequence named `<name>-modified` (override
|
|
531
|
+
with `newName`); pass `overwrite: true` to edit the sequence in place.
|
|
532
|
+
|
|
533
|
+
### Closing Chrome Afterwards
|
|
534
|
+
|
|
535
|
+
```javascript
|
|
536
|
+
replay({ action: 'run', name: 'smoke-test', connectionReason: 'ci-run',
|
|
537
|
+
killChromeOnFinish: true })
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
`killChromeOnFinish` kills the browsers the run **owns**: its own (run-level)
|
|
541
|
+
connection, plus every browser a `launchChrome` step actually created. It runs
|
|
542
|
+
only after the run finishes - it is skipped on pause, breakpoint,
|
|
543
|
+
click-validation failure or abort.
|
|
544
|
+
|
|
545
|
+
Ownership is read from the launch itself, not guessed from the sequence: a
|
|
546
|
+
`launchChrome` step against a reference that already exists hands back someone
|
|
547
|
+
else's browser (`CHROME_CONNECTION_REUSED`), and those are left running - that
|
|
548
|
+
is the long-lived instance you started by hand, and killing it would take state
|
|
549
|
+
you cannot get back.
|
|
550
|
+
|
|
551
|
+
For the same reason the kill is **skipped entirely when another live connection
|
|
552
|
+
shares the port** — a `launchChrome` step normally opens a tab in the existing
|
|
553
|
+
instance rather than a new process, so killing by port would take those
|
|
554
|
+
browsers down too. The run says so instead: *"Chrome left running (port 9224
|
|
555
|
+
also serves duo-member-two, killChromeOnFinish)"*. Disconnect or close the other
|
|
556
|
+
connections first if you want the instance gone.
|
|
557
|
+
|
|
558
|
+
### Preview Sequence
|
|
559
|
+
|
|
560
|
+
```javascript
|
|
561
|
+
replay({ action: 'get', name: 'login-flow' })
|
|
562
|
+
// Shows: commands, substitutable variables, metadata, and run instructions
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## Variables: Two Unrelated Mechanisms
|
|
566
|
+
|
|
567
|
+
`replay` has two features that both get called "variables". They share nothing
|
|
568
|
+
but the word.
|
|
569
|
+
|
|
570
|
+
| | `variables` on `run` | `saveAs` + `{{var:...}}` |
|
|
571
|
+
|---|---|---|
|
|
572
|
+
| What it does | Replaces **recorded typed text** before the run | Captures a **value produced mid-run** for later steps |
|
|
573
|
+
| Set where | A parameter of `replay({ action: 'run' })` | A param on an individual sequence step |
|
|
574
|
+
| Read where | Only by `input({ action: 'type' })` steps | Any step param, via `{{var:name}}` |
|
|
575
|
+
| Naming | Auto-generated `var_<i>_<selector>` keys | Names you choose |
|
|
576
|
+
|
|
577
|
+
### 1. `variables` - substituting recorded typed text
|
|
578
|
+
|
|
579
|
+
Every recorded `input({ action: 'type' })` step gets an auto-generated key of
|
|
580
|
+
the form `var_<0-based step index>_<selector, non-alphanumerics replaced by _>`
|
|
581
|
+
(or `var_<i>_text` when the step has no selector).
|
|
582
|
+
|
|
583
|
+
```javascript
|
|
584
|
+
// Original recording had: input({ action: 'type', selector: '#email', text: 'original@email.com' })
|
|
585
|
+
replay({
|
|
586
|
+
action: 'run',
|
|
587
|
+
sequenceId: 'seq-login-flow',
|
|
588
|
+
connectionReason: 'test-session',
|
|
589
|
+
variables: {
|
|
590
|
+
'var_2_#email': 'new@email.com',
|
|
591
|
+
'var_3_#password': 'newpassword'
|
|
592
|
+
}
|
|
593
|
+
})
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**If the sequence contains any typed text and you omit `variables` entirely,
|
|
597
|
+
`run` does not execute** - it returns a prompt listing the substitutable keys
|
|
598
|
+
and their recorded values. Pass `variables: {}` to accept the recorded values
|
|
599
|
+
as-is, or supply the keys you want to change.
|
|
600
|
+
|
|
601
|
+
### 2. `saveAs` and `{{var:name.path}}` - capturing values mid-run
|
|
602
|
+
|
|
603
|
+
A step can capture its own result into the run's variable store with `saveAs`.
|
|
604
|
+
Later steps read it back with `{{var:name}}` / `{{var:name.path}}` in any
|
|
605
|
+
param.
|
|
606
|
+
|
|
607
|
+
Supported on exactly two tools today:
|
|
608
|
+
|
|
609
|
+
| Step | What gets stored |
|
|
610
|
+
|---|---|
|
|
611
|
+
| `request` | the **whole response object** - address into it: `{{var:login.body.token}}` |
|
|
612
|
+
| `inspect({ action: 'evaluateExpression' })` | the **evaluated value itself** - use it directly: `{{var:pairingUrl}}` |
|
|
613
|
+
|
|
614
|
+
```json
|
|
615
|
+
{ "tool": "request", "params": {
|
|
616
|
+
"url": "https://api.example.com/login", "method": "POST", "saveAs": "login" } }
|
|
617
|
+
|
|
618
|
+
{ "tool": "inspect", "params": {
|
|
619
|
+
"action": "evaluateExpression",
|
|
620
|
+
"expression": "document.querySelector('#pair').href",
|
|
621
|
+
"saveAs": "pairingUrl" } }
|
|
622
|
+
|
|
623
|
+
{ "tool": "navigate", "params": { "action": "goto", "url": "{{var:pairingUrl}}" } }
|
|
624
|
+
|
|
625
|
+
{ "tool": "assert", "params": {
|
|
626
|
+
"left": "{{var:login.body.token}}", "operator": "exists" } }
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
Behaviour worth knowing:
|
|
630
|
+
|
|
631
|
+
- A `saveAs` that cannot be honoured (unsupported tool, or a call that produced
|
|
632
|
+
nothing capturable) **fails the step**. It is never a silent no-op, because the
|
|
633
|
+
failure would otherwise surface far away as a confusing "no variable named ..."
|
|
634
|
+
message.
|
|
635
|
+
- Async expressions work: a Promise returned by `evaluateExpression` is awaited
|
|
636
|
+
by default, so an async IIFE (IndexedDB read, `crypto.subtle`, `fetch`)
|
|
637
|
+
captures its **settled value**, not the Promise object. A rejection fails the
|
|
638
|
+
step with the expression's own error.
|
|
639
|
+
- JSON-serializable results are captured **by value** (exact - a string `"42"`
|
|
640
|
+
stays a string). Values that only render as a description (`[HTMLDivElement]`,
|
|
641
|
+
`Array(3)`) come back as strings - capture a specific field, not a whole DOM
|
|
642
|
+
object.
|
|
643
|
+
- The store is shared by reference across the whole run, including nested
|
|
644
|
+
`conditional` sequences and steps running on other connections, and it
|
|
645
|
+
survives a mid-run pause into `step`/`finish`.
|
|
646
|
+
|
|
647
|
+
### Interpolation Tokens
|
|
648
|
+
|
|
649
|
+
`{{var:...}}` and `{{timestamp}}` are resolved in a step's params immediately
|
|
650
|
+
before it executes.
|
|
651
|
+
|
|
652
|
+
- `{{timestamp}}` - milliseconds, computed **once per run** and reused by every
|
|
653
|
+
step (including later `step`/`finish` calls), so all steps agree on it.
|
|
654
|
+
- `{{timestamp+3600000}}` / `{{timestamp-1000}}` - the same run timestamp with a
|
|
655
|
+
millisecond offset, for expiry-style fields.
|
|
656
|
+
- `{{var:a.b.c}}` - dot-separated path. For a key containing a literal dot, use
|
|
657
|
+
bracket notation for that segment: `{{var:a.b['exec.t1.s2'].c}}`.
|
|
658
|
+
- **Whole-string tokens keep their type.** `"right": "{{var:r.body.count}}"`
|
|
659
|
+
resolves to the number `3`; `"name": "kit{{timestamp}}"` resolves to the
|
|
660
|
+
string `"kit1699999999999"`.
|
|
661
|
+
- An unresolvable token fails the step with a message telling you which
|
|
662
|
+
`saveAs` to add.
|
|
663
|
+
|
|
664
|
+
## Per-Step Connections (Multi-Device Sequences)
|
|
665
|
+
|
|
666
|
+
Any step may carry its own `connectionReason`. Steps that don't get the
|
|
667
|
+
run-level connection injected (for the tools that accept one).
|
|
668
|
+
|
|
669
|
+
```json
|
|
670
|
+
{ "tool": "input", "params": { "action": "click", "selector": "#pair",
|
|
671
|
+
"connectionReason": "device-a" } }
|
|
672
|
+
{ "tool": "inspect", "params": { "action": "evaluateExpression",
|
|
673
|
+
"expression": "document.querySelector('#code').textContent",
|
|
674
|
+
"saveAs": "code", "connectionReason": "device-a" } }
|
|
675
|
+
{ "tool": "navigate", "params": { "action": "goto", "url": "{{var:code}}",
|
|
676
|
+
"connectionReason": "device-b" } }
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
A per-step connection is honoured for **everything wrapped around the step**,
|
|
680
|
+
not just for dispatching it: pre/post-click state capture, click validation,
|
|
681
|
+
navigation validation, typed-text validation, breakpoint/pause detection,
|
|
682
|
+
failure diagnostics, and the wait for the *next* step's element (which follows
|
|
683
|
+
the next step's connection). That is what makes "device A scans, device B
|
|
684
|
+
confirms" work in a single run.
|
|
685
|
+
|
|
686
|
+
Details:
|
|
687
|
+
|
|
688
|
+
- Connection injection applies to `navigate`, `content`, `input`, `console`,
|
|
689
|
+
`network`, `dom`, `screenshot`, `storage`, `inspect`, `execution`,
|
|
690
|
+
`breakpoint`, `getSourceCode`, `detectModals`, `dismissModal`.
|
|
691
|
+
- `request` is handled separately: it only receives a connection when the step
|
|
692
|
+
sets `destination: 'browser'`, so Node-targeted sequences never drag a Chrome
|
|
693
|
+
launch in.
|
|
694
|
+
- Only the browser-only tools (the first group above) make a sequence
|
|
695
|
+
"need a connection" and trigger Chrome auto-launch. A Node-only debugging
|
|
696
|
+
sequence won't spuriously launch Chrome.
|
|
697
|
+
### Recording a multi-connection sequence
|
|
698
|
+
|
|
699
|
+
Recording **preserves** `connectionReason` (it used to be stripped, which meant
|
|
700
|
+
a recorded two-browser sequence silently replayed in one). Pass it explicitly on
|
|
701
|
+
**every** call while you drive the browsers, including the one that is already
|
|
702
|
+
active, then `create` decides what to do with it:
|
|
703
|
+
|
|
704
|
+
| Recording | `create` result |
|
|
705
|
+
|---|---|
|
|
706
|
+
| All steps on one connection | Hoisted off the steps, so the sequence stays portable and `run({ connectionReason })` still retargets it |
|
|
707
|
+
| Genuinely spans connections | Kept per step |
|
|
708
|
+
| **Mixed** — some steps named, others driven implicitly | Kept as-is, with a warning |
|
|
709
|
+
|
|
710
|
+
The mixed case can't be resolved automatically: nothing knows which browser the
|
|
711
|
+
bare steps belonged to, so hoisting could pin them to the wrong one. `create`
|
|
712
|
+
says so in its output; re-record naming every step rather than shipping it.
|
|
713
|
+
|
|
714
|
+
"Bare" means any step that would have the run-level connection injected — which
|
|
715
|
+
includes the tools whose `connectionReason` is *optional* (`inspect`,
|
|
716
|
+
`execution`, `storage`, `network`, `breakpoint`, `request`, `getSourceCode`),
|
|
717
|
+
not just the browser-only ones. Those are the ones people actually leave off.
|
|
718
|
+
`wait({ ms })` is a plain sleep and doesn't count; every other `wait` form does.
|
|
719
|
+
|
|
720
|
+
A sequence can be both multi-connection **and** mixed, and that combination is
|
|
721
|
+
the dangerous one: the bare steps land in a different browser depending on the
|
|
722
|
+
run-level `connectionReason`, and the run reports success either way. `create`
|
|
723
|
+
warns about both.
|
|
724
|
+
|
|
725
|
+
### Inserting into an existing sequence
|
|
726
|
+
|
|
727
|
+
`insert` splices history commands (which now carry their connection) into a
|
|
728
|
+
sequence whose own steps had theirs hoisted off. To compare like with like it
|
|
729
|
+
first re-stamps the hoisted connection — recorded on the sequence as
|
|
730
|
+
`recordedConnection` — onto its bare steps, then re-normalizes the merged array:
|
|
731
|
+
|
|
732
|
+
| Inserted steps came from | Result |
|
|
733
|
+
|---|---|
|
|
734
|
+
| The same browser as the sequence | Hoisted again — still portable |
|
|
735
|
+
| A different browser | Every step made explicit, so the sequence is a real multi-connection one and the run-time existence check applies |
|
|
736
|
+
|
|
737
|
+
Without that re-stamp the merge always looked "mixed" (one named reference plus
|
|
738
|
+
the sequence's own bare steps), the hoist was skipped, and an ordinary
|
|
739
|
+
same-browser insert silently left the sequence half-pinned to this session — so
|
|
740
|
+
a later `run({ connectionReason })` split it across two browsers and passed.
|
|
741
|
+
|
|
742
|
+
### Rebinding references at run time
|
|
743
|
+
|
|
744
|
+
Recorded references are per-session, so a sequence recorded elsewhere needs its
|
|
745
|
+
names mapped onto this session's:
|
|
746
|
+
|
|
747
|
+
```javascript
|
|
748
|
+
replay({
|
|
749
|
+
action: 'run',
|
|
750
|
+
sequenceId: 'duo-stock-propagation',
|
|
751
|
+
connections: { 'duo-member-two': 'my-second-browser' },
|
|
752
|
+
})
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
Recorded name on the left, a reference from this session on the right. Both
|
|
756
|
+
sides are sanitized, so spaced forms work. A key matching nothing in the
|
|
757
|
+
sequence is rejected before anything runs, listing the references the sequence
|
|
758
|
+
actually uses — a typo fails loudly instead of being ignored. "The sequence"
|
|
759
|
+
includes any sequence reached through a `conditional` step, since a setup
|
|
760
|
+
sequence normally lives behind one; when such a sub-sequence can't be resolved
|
|
761
|
+
in memory the key is accepted rather than guessed at.
|
|
762
|
+
|
|
763
|
+
A step naming a connection other than the run's is checked against the live
|
|
764
|
+
session before it runs, whether or not the sequence spans several connections,
|
|
765
|
+
so a missing browser fails as *"step 3 needs connection duo-member-two, which
|
|
766
|
+
does not exist in this session"* instead of a generic "not connected to browser"
|
|
767
|
+
from inside the tool.
|
|
768
|
+
|
|
769
|
+
Mapping also renames the `reference` on `launchChrome` / `connectDebugger`
|
|
770
|
+
steps; otherwise a mapped sequence would launch the recorded name and then drive
|
|
771
|
+
a different one. Where a mapping renames a launch, it wins over the run-level
|
|
772
|
+
`connectionReason`, which would otherwise rename it straight back.
|
|
773
|
+
|
|
774
|
+
**Two recorded references cannot be mapped onto one browser.** That would run
|
|
775
|
+
the whole multi-browser sequence in a single browser and report success — the
|
|
776
|
+
original bug, re-entered through the API that exists to prevent it — so it is
|
|
777
|
+
rejected before anything runs.
|
|
778
|
+
|
|
779
|
+
The run-level connection is mapped too when it was *derived* from the sequence
|
|
780
|
+
(e.g. from a `launchChrome` step) rather than passed explicitly; otherwise it
|
|
781
|
+
would point at a reference that doesn't exist here and the `startUrl` navigation
|
|
782
|
+
and cursor injection would silently no-op.
|
|
783
|
+
|
|
784
|
+
`issues({ action: 'workOn' | 'resolve' })` accepts `connections` as well, so a
|
|
785
|
+
multi-browser repro sequence attached to an issue can be replayed in a fresh
|
|
786
|
+
session.
|
|
787
|
+
|
|
788
|
+
### Declaring the browsers a sequence needs
|
|
789
|
+
|
|
790
|
+
A multi-browser sequence can say which browsers it needs, instead of expecting
|
|
791
|
+
whoever runs it to have launched them first. Set it with `declare`:
|
|
792
|
+
|
|
793
|
+
```javascript
|
|
794
|
+
replay({
|
|
795
|
+
action: 'declare',
|
|
796
|
+
name: 'duo-stock-propagation',
|
|
797
|
+
requiredConnections: [
|
|
798
|
+
{ reference: 'duo-member-two', profile: 'device-a', role: 'the member who draws stock' }
|
|
799
|
+
],
|
|
800
|
+
requiredSockets: ['/api/sync/socket'],
|
|
801
|
+
})
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
Each list **replaces** its field and `[]` clears it — a declaration is a whole
|
|
805
|
+
statement about the run, and merging would make "drop the second browser"
|
|
806
|
+
unexpressible. Passing only one list leaves the other alone. The sequence is
|
|
807
|
+
written back to the file it came from; a memory-only sequence waits for
|
|
808
|
+
`export`. Declarations that cannot mean what they say (two references on one
|
|
809
|
+
profile, a reference declared twice, a profile name that is not a safe
|
|
810
|
+
directory segment, an empty socket pattern that would match every socket) are
|
|
811
|
+
refused here rather than on the next run.
|
|
812
|
+
|
|
813
|
+
It lands on the sequence next to `commands`:
|
|
814
|
+
|
|
815
|
+
```json
|
|
816
|
+
{
|
|
817
|
+
"name": "duo-stock-propagation",
|
|
818
|
+
"startUrl": "http://localhost:5173/",
|
|
819
|
+
"requiredConnections": [
|
|
820
|
+
{ "reference": "duo-member-two", "role": "the member who draws stock",
|
|
821
|
+
"url": "http://localhost:5173/login" }
|
|
822
|
+
],
|
|
823
|
+
"commands": [ ... ]
|
|
824
|
+
}
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
| Field | Meaning |
|
|
828
|
+
|---|---|
|
|
829
|
+
| `reference` | The reference the steps use |
|
|
830
|
+
| `profile` | Named persistent Chrome profile to come up on (see below) |
|
|
831
|
+
| `url` | Opened on launch (defaults to the sequence's `startUrl`) |
|
|
832
|
+
| `forceNewInstance` | A separate browser process rather than a tab, default **true** — two identities sharing one browser share its storage, which defeats the point. Defaults to **false** when `profile` is set |
|
|
833
|
+
| `role` | Why this browser exists, for the run summary |
|
|
834
|
+
|
|
835
|
+
The run brings each one up before the first step. A reference already bound to
|
|
836
|
+
a live browser is reused, so a browser you launched by hand is not duplicated,
|
|
837
|
+
and a `connections` mapping wins over the declaration — the declaration
|
|
838
|
+
supplies a default, it does not override where the caller points the steps. A
|
|
839
|
+
browser that cannot be launched fails the run before any step executes, naming
|
|
840
|
+
the reference and its role.
|
|
841
|
+
|
|
842
|
+
The run **closes what it launched**, on every terminal outcome: completed,
|
|
843
|
+
failed, and cancelled. A pause is the exception — those browsers are the state
|
|
844
|
+
you stopped to inspect — but whatever ends the pause (`cancel`, `finish`, or
|
|
845
|
+
stepping off the end) closes them then. Browsers that were already up are left
|
|
846
|
+
alone, and a browser sharing its port with another live connection is left
|
|
847
|
+
running too. The run says which it closed:
|
|
848
|
+
|
|
849
|
+
```
|
|
850
|
+
**Browsers closed** (declared and launched): duo-member-two
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
#### Declaring the device, not just the browser
|
|
854
|
+
|
|
855
|
+
`profile` names a persistent Chrome profile (the same ones
|
|
856
|
+
`launchChrome({ profile })` creates, under `~/.cdp-tools/profiles`):
|
|
857
|
+
|
|
858
|
+
```json
|
|
859
|
+
"requiredConnections": [
|
|
860
|
+
{ "reference": "device-a", "profile": "device-a", "role": "the enrolled device" }
|
|
861
|
+
]
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
The profile is the durable half. Its cookies, localStorage and IndexedDB —
|
|
865
|
+
including non-extractable CryptoKeys — survive between runs, so a device
|
|
866
|
+
enrolled once stays enrolled, while the reference is only a name for this
|
|
867
|
+
session. Declaring the pair is what lets a saved multi-device sequence be re-run
|
|
868
|
+
tomorrow without rewiring which reference means which device.
|
|
869
|
+
|
|
870
|
+
Steps still address browsers by `connectionReason`. There is no per-step
|
|
871
|
+
`profile`: a step names a browser, the declaration decides what that browser
|
|
872
|
+
is.
|
|
873
|
+
|
|
874
|
+
Two consequences worth knowing:
|
|
875
|
+
|
|
876
|
+
- **A profile implies reuse.** Only one live Chrome may hold a profile, so
|
|
877
|
+
`forceNewInstance` defaults to `false` here — a Chrome already running that
|
|
878
|
+
profile *is* the browser the declaration wants, whatever reference it is
|
|
879
|
+
bound to. Set `forceNewInstance: true` explicitly if you really want a spawn
|
|
880
|
+
attempt.
|
|
881
|
+
- **A profile-bearing reference cannot be rebound.** Elsewhere a `connections`
|
|
882
|
+
mapping wins over a declaration, because a declaration is only a default. A
|
|
883
|
+
profile is an identity claim: pointing `device-a` at another browser would run
|
|
884
|
+
device-a's steps somewhere that is not device-a and report success. The run is
|
|
885
|
+
refused instead. Two declarations naming the same profile are refused for the
|
|
886
|
+
same reason — they would be one browser wearing two names.
|
|
887
|
+
|
|
888
|
+
Teardown kills the browser, never the profile: the directory is persistent, so
|
|
889
|
+
the next run finds the device exactly as this one left it.
|
|
890
|
+
|
|
891
|
+
### Saying what kind of sequence it is
|
|
892
|
+
|
|
893
|
+
`tags` are free-form labels the suite runner selects on:
|
|
894
|
+
|
|
895
|
+
```javascript
|
|
896
|
+
replay({ action: 'declare', name: 'spine-09-retire-asset', tags: ['ui'] })
|
|
897
|
+
replay({ action: 'declare', name: 'story-b1-pool', tags: ['contract', 'slow'] })
|
|
898
|
+
|
|
899
|
+
replay({ action: 'runAll', tags: ['ui'] }) // only those
|
|
900
|
+
replay({ action: 'runAll', folder: 'spine', tags: ['ui'] }) // composes with folder
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
Several tags mean **any of**. Tags are lowercased, trimmed and de-duplicated on
|
|
904
|
+
the way in — a tag is matched, not displayed, and `tags: ['UI']` skipping a
|
|
905
|
+
sequence tagged `ui` would quietly run less than you asked for. Spaces are
|
|
906
|
+
refused for the same reason: `slow ui` is ambiguous in a filter, `slow-ui`
|
|
907
|
+
isn't.
|
|
908
|
+
|
|
909
|
+
Every `runAll` reports the split, filtered or not:
|
|
910
|
+
|
|
911
|
+
```
|
|
912
|
+
runAll folder "tagcheck": 3 passed, 0 failed (1 contract, 1 ui, 1 untagged)
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
That line is the point of the feature. A suite of 43 sequences reporting "36
|
|
916
|
+
passed" reads as interface coverage, and 14 of those may never issue an `input`
|
|
917
|
+
step — `navigate` → `request` → `assert`, with the browser present only to hold
|
|
918
|
+
the auth cookie. Good contract tests, but no UI regression can fail one of
|
|
919
|
+
them. The split makes the balance visible on every run instead of on an audit.
|
|
920
|
+
|
|
921
|
+
Folders can't carry this: they're already spoken for by scenario shape
|
|
922
|
+
(`spine/`, `story/`, `_helpers/`), and a sequence has one folder but can
|
|
923
|
+
legitimately be both `contract` and `slow`.
|
|
924
|
+
|
|
925
|
+
### Declaring the sockets a sequence depends on
|
|
926
|
+
|
|
927
|
+
`requiredSockets` is the same idea for transports: a list of URL substrings
|
|
928
|
+
naming the WebSockets the sequence's assertions ride on. Set by the same
|
|
929
|
+
action:
|
|
930
|
+
|
|
931
|
+
```javascript
|
|
932
|
+
replay({ action: 'declare', name: 'duo-stock-propagation',
|
|
933
|
+
requiredSockets: ['/api/sync/socket'] })
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
A sequence that declares them is checked whether or not the caller asks for it
|
|
937
|
+
(`requireSockets: true` is only needed for a sequence that declares none). For
|
|
938
|
+
each entry the run fails when a matching socket closed or hit frame errors
|
|
939
|
+
while it executed, or when none is open at the end — including one that never
|
|
940
|
+
opened at all, which no "is it up now" final assertion can catch. Closes the
|
|
941
|
+
run did not cause are not blamed on it: a socket torn down with its target by a
|
|
942
|
+
navigation, or hung up by the page itself, is normal.
|
|
943
|
+
|
|
944
|
+
Match on the app's own path rather than the origin, so the declaration survives
|
|
945
|
+
`baseUrl` retargeting. Dev-server sockets (Vite HMR and friends) simply go
|
|
946
|
+
undeclared and are ignored.
|
|
947
|
+
|
|
948
|
+
### repeat / runFromLog
|
|
949
|
+
|
|
950
|
+
History retains the connection each command was recorded with, so both replay
|
|
951
|
+
each command against its own connection by default. An explicit
|
|
952
|
+
`connectionReason`:
|
|
953
|
+
|
|
954
|
+
- **retargets** a batch that used a single connection (what the parameter has
|
|
955
|
+
always meant), and
|
|
956
|
+
- is **refused** for a batch spanning several connections, since no single value
|
|
957
|
+
is honest there.
|
|
958
|
+
|
|
959
|
+
It is deliberately not silently ignored — for a while it was, which broke a
|
|
960
|
+
documented parameter with no signal at all.
|
|
961
|
+
|
|
962
|
+
### Exported test code
|
|
963
|
+
|
|
964
|
+
`outputFormat: 'playwright' | 'puppeteer'` gives each recorded connection its own
|
|
965
|
+
page (`page`, `pageDuoMemberTwo`, …), with a header naming the browsers. A
|
|
966
|
+
single-connection sequence generates exactly what it always did. Emitting every
|
|
967
|
+
step against one `page` would relocate the same silent collapse into the
|
|
968
|
+
exported test.
|
|
969
|
+
|
|
970
|
+
The generators only know `navigate` and `input` steps. Anything else —
|
|
971
|
+
`conditional`, `launchChrome`, `inspect`, `storage`, `wait`, `breakpoint` —
|
|
972
|
+
becomes a `// [not generated]` comment naming the step, and a sequence where
|
|
973
|
+
*nothing* could be generated exports a test that **throws** rather than an empty
|
|
974
|
+
one that passes. A setup sequence made of a conditional and a launch has no
|
|
975
|
+
Playwright equivalent at all; run it with `replay({ action: 'run' })` instead of
|
|
976
|
+
exporting it.
|
|
977
|
+
|
|
978
|
+
### Two deliberate non-behaviours
|
|
979
|
+
|
|
980
|
+
- **A run-level `connectionReason` does not override a step's own.** The step
|
|
981
|
+
wins.
|
|
982
|
+
- **A per-step reference that doesn't exist fails the step.** It never falls
|
|
983
|
+
back to the run-level connection.
|
|
984
|
+
|
|
985
|
+
The second is the whole point. Falling back is what let a two-browser sequence
|
|
986
|
+
replay in one browser and report success — the "member" steps ran in the owner's
|
|
987
|
+
browser, the owner saw their own optimistic update, and a cross-user propagation
|
|
988
|
+
assertion went green having never involved a second user.
|
|
989
|
+
|
|
990
|
+
## Conditional Steps
|
|
991
|
+
|
|
992
|
+
`conditional` is a virtual step tool: it is handled inside the executor, never
|
|
993
|
+
appears in the tool list, and is exempt from tool-name validation.
|
|
994
|
+
|
|
995
|
+
Not being a real tool, it is never recorded, so `create` and `insert` — which
|
|
996
|
+
both build steps out of recorded history — cannot produce one. `addConditional`
|
|
997
|
+
is its authoring action:
|
|
998
|
+
|
|
999
|
+
```javascript
|
|
1000
|
+
replay({ action: 'addConditional',
|
|
1001
|
+
name: 'checkout-flow', // or sequenceId
|
|
1002
|
+
condition: '{{selector:.cookie-banner}}',
|
|
1003
|
+
thenSequence: 'dismiss-cookie-banner',
|
|
1004
|
+
insertAfterStep: 2, // omit to append; 0 puts it first
|
|
1005
|
+
comment: 'EU builds only' }) // optional
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
which stores the step as:
|
|
1009
|
+
|
|
1010
|
+
```json
|
|
1011
|
+
{ "tool": "conditional", "params": {
|
|
1012
|
+
"if": "{{selector:.cookie-banner}}",
|
|
1013
|
+
"then": "dismiss-cookie-banner" } }
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
Rejected before the sequence is touched: a condition that doesn't parse, an
|
|
1017
|
+
unknown type, an uncompilable or over-long `url:matches` regex, a malformed
|
|
1018
|
+
`indexedDB` path, a `thenSequence` naming no known sequence or naming this one
|
|
1019
|
+
(which would recurse to `maxConditionalDepth`), an out-of-range
|
|
1020
|
+
`insertAfterStep`. Values holding a `{{var:...}}` token are skipped — they are
|
|
1021
|
+
substituted at run time.
|
|
1022
|
+
|
|
1023
|
+
A sequence already saved on disk is rewritten in place; otherwise it waits for
|
|
1024
|
+
`export`. The response says which.
|
|
1025
|
+
|
|
1026
|
+
`then` is the name of another sequence, loaded and run inline when the condition
|
|
1027
|
+
holds, and it shares the parent run's captured variables. A `launchChrome` step
|
|
1028
|
+
inside it is skipped when that reference is already connected, and run when it
|
|
1029
|
+
isn't - so a setup sequence spanning two browsers can create the second one.
|
|
1030
|
+
|
|
1031
|
+
Which browser the sub-sequence's *bare* steps run in follows from that:
|
|
1032
|
+
|
|
1033
|
+
| The nested `launchChrome` | Bare steps run in |
|
|
1034
|
+
|---|---|
|
|
1035
|
+
| **ran** (that browser didn't exist) | the browser it just launched |
|
|
1036
|
+
| **skipped** (already connected), or absent | the calling run's connection |
|
|
1037
|
+
|
|
1038
|
+
That split is what makes both shapes work. A setup sequence is a launch plus
|
|
1039
|
+
bare steps (`create` hoists the connection off them), so its steps have to
|
|
1040
|
+
follow the browser it created - otherwise the run opens a browser, does the work
|
|
1041
|
+
in the *caller's* browser, and still reports success. A nested login sequence
|
|
1042
|
+
whose browser already exists keeps running in whatever browser called it. Steps
|
|
1043
|
+
that name their own `connectionReason` are unaffected either way.
|
|
1044
|
+
|
|
1045
|
+
> **Two connections are not two devices.** A plain `launchChrome` reuses the
|
|
1046
|
+
> running instance and opens a *tab* in it, so both references share one profile
|
|
1047
|
+
> - one set of cookies, one localStorage, one IndexedDB. A duo test built that
|
|
1048
|
+
> way has a single device identity wearing two names, and a "does it propagate
|
|
1049
|
+
> to the other user" check passes without a second device ever existing. Give
|
|
1050
|
+
> the second browser its own `profile` (and `forceNewInstance: true`) when the
|
|
1051
|
+
> point of the test is that the two sides are genuinely separate:
|
|
1052
|
+
>
|
|
1053
|
+
> ```javascript
|
|
1054
|
+
> launchChrome({ reference: 'duo-member-two', profile: 'member', forceNewInstance: true })
|
|
1055
|
+
> ```
|
|
1056
|
+
>
|
|
1057
|
+
> `listConnections` shows the giveaway: same `port` means same instance and
|
|
1058
|
+
> therefore shared storage.
|
|
1059
|
+
|
|
1060
|
+
Supported conditions:
|
|
1061
|
+
|
|
1062
|
+
| Condition | True when |
|
|
1063
|
+
|---|---|
|
|
1064
|
+
| `{{selector:CSS}}` / `{{!selector:CSS}}` | element exists / doesn't |
|
|
1065
|
+
| `{{url:contains:STRING}}` | current URL contains the string |
|
|
1066
|
+
| `{{url:matches:REGEX}}` | current URL matches the regex |
|
|
1067
|
+
| `{{url:EXACT}}` | current URL equals the value |
|
|
1068
|
+
| `{{cookie:NAME}}` / `{{!cookie:NAME}}` | cookie exists / doesn't |
|
|
1069
|
+
| `{{localStorage:KEY}}` / `{{!localStorage:KEY}}` | key exists / doesn't |
|
|
1070
|
+
| `{{indexedDB:DB/STORE/KEY}}` / `{{!indexedDB:...}}` | that record exists / doesn't |
|
|
1071
|
+
| `{{indexedDB:DB/STORE}}` | the object store holds at least one record |
|
|
1072
|
+
|
|
1073
|
+
An element that isn't on the page counts as *absent*, not an error, so
|
|
1074
|
+
`{{!selector:...}}` skips correctly. A malformed selector or a disconnected
|
|
1075
|
+
browser still fails the run. The page is probed once with no retry (precede an
|
|
1076
|
+
async marker with a `wait` step), and a hidden element counts as present.
|
|
1077
|
+
|
|
1078
|
+
Every condition reads the tool's structured result, never its printed text, so
|
|
1079
|
+
stored *data* cannot answer a question about *structure*: a localStorage value
|
|
1080
|
+
of `"null"` (or one containing "not found") is present, an empty string is
|
|
1081
|
+
present, a cookie name matches exactly rather than as a suffix, and a URL
|
|
1082
|
+
containing a comma compares in full.
|
|
1083
|
+
|
|
1084
|
+
A database or store that doesn't exist yet counts as *absent*, not as an
|
|
1085
|
+
evaluation error - that is the state a wiped profile is in, and the state a
|
|
1086
|
+
healing setup sequence exists to fix. A value that cannot be represented in
|
|
1087
|
+
JSON (a non-extractable `CryptoKey`, a `Blob`) still counts as present.
|
|
1088
|
+
Presence comes from the storage tool's structured result, not its printed text,
|
|
1089
|
+
so a record whose *value* happens to read "No record found for this key." is
|
|
1090
|
+
still present. A condition is written as text, so an all-digits key is probed as
|
|
1091
|
+
a string and then, if that misses, as a number - IndexedDB keys `42` and `"42"`
|
|
1092
|
+
are different keys.
|
|
1093
|
+
|
|
1094
|
+
A condition is interpolated like any other step parameter, so a captured
|
|
1095
|
+
variable can drive it — `{{indexedDB:identity/keys/{{var:deviceId}}}}` after an
|
|
1096
|
+
earlier `inspect({ saveAs: 'deviceId' })`.
|
|
1097
|
+
|
|
1098
|
+
A condition that is legitimately *not met* skips the nested sequence and the
|
|
1099
|
+
step counts as a success. A condition that cannot be *evaluated* (bad format,
|
|
1100
|
+
unknown type, invalid or over-long regex, tool error) fails the run.
|
|
1101
|
+
|
|
1102
|
+
Nesting is capped by `replay.maxConditionalDepth` (default 10) and regexes by
|
|
1103
|
+
`replay.maxRegexLength` (default 500); both are `.cdp-tools/config.json`
|
|
1104
|
+
settings. Oscillating chains (A→B→A) are allowed up to the depth limit.
|
|
1105
|
+
|
|
1106
|
+
## forEach Steps
|
|
1107
|
+
|
|
1108
|
+
`forEach` is the second virtual step tool: handled inside the executor, never a
|
|
1109
|
+
registered tool, exempt from tool-name validation via `VIRTUAL_STEP_TOOLS`.
|
|
1110
|
+
|
|
1111
|
+
```json
|
|
1112
|
+
{ "tool": "forEach", "params": {
|
|
1113
|
+
"in": "{{var:shares}}",
|
|
1114
|
+
"as": "share",
|
|
1115
|
+
"do": "revoke-one-share",
|
|
1116
|
+
"where": "item.name !== 'Employees'",
|
|
1117
|
+
"maxItems": 50 } }
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
It exists because conditions are single-subject. `{{selector:X}}` answers "does
|
|
1121
|
+
X exist"; there is no condition that answers "what is there", so a sequence could
|
|
1122
|
+
provision a missing fixture but never remove an unexpected one. That asymmetry is
|
|
1123
|
+
what made shared-fixture suites drift.
|
|
1124
|
+
|
|
1125
|
+
`in` resolves in one of two ways. `{{var:name}}` is a whole-string interpolation
|
|
1126
|
+
token, so **the run's normal param interpolation resolves it before the step is
|
|
1127
|
+
dispatched** and — because whole-string tokens preserve type — what the step
|
|
1128
|
+
receives is the captured array itself, not a string. `resolveForEachItems`
|
|
1129
|
+
therefore accepts an array directly; its string branches only matter for direct
|
|
1130
|
+
calls and for `{{selectorAll:CSS}}`, which is not a var token and arrives
|
|
1131
|
+
unresolved. `{{selectorAll:CSS}}` evaluates in the page and yields one plain
|
|
1132
|
+
descriptor per element (`index`, `text`, `id`, `className`, `href`, `value`) —
|
|
1133
|
+
DOM nodes cannot cross the CDP boundary.
|
|
1134
|
+
|
|
1135
|
+
`where` is evaluated as JavaScript in the page with `item` and `index` in scope,
|
|
1136
|
+
**not** in the `{{...}}` condition grammar. A filter reads fields off an
|
|
1137
|
+
arbitrary object, which that grammar cannot express, and adding a second
|
|
1138
|
+
mini-language beside it would leave two half-expressive syntaxes. A `where` that
|
|
1139
|
+
throws fails the run rather than excluding the item, matching how an
|
|
1140
|
+
unevaluatable condition behaves.
|
|
1141
|
+
|
|
1142
|
+
Per-iteration the item is written to the shared variable store under `as` (and
|
|
1143
|
+
its position under `<as>Index`). The store is shared by reference with nested
|
|
1144
|
+
runs, so bindings are replaced rather than scoped, and a body's own `saveAs`
|
|
1145
|
+
captures persist across iterations.
|
|
1146
|
+
|
|
1147
|
+
Budget is the parent's *remaining* total, decremented per iteration, so a loop
|
|
1148
|
+
cannot extend the run's total the way a fresh copy would. Depth shares
|
|
1149
|
+
`maxConditionalDepth`: a loop body that loops is the same runaway risk as a
|
|
1150
|
+
conditional chain. `maxItems` (default 100) is a backstop, and hitting it is
|
|
1151
|
+
logged rather than silently truncating.
|
|
1152
|
+
|
|
1153
|
+
An empty source is a success with `iterations: 0`, rendered as "N item(s) found,
|
|
1154
|
+
none ran" — a converge loop with nothing to do must not look like a broken
|
|
1155
|
+
selector.
|
|
1156
|
+
|
|
1157
|
+
## Teardown Steps
|
|
1158
|
+
|
|
1159
|
+
`CommandSequence.teardown` is an optional second command array, run by
|
|
1160
|
+
`runTeardown` after the main loop in `executeSteps` reaches a terminal state.
|
|
1161
|
+
|
|
1162
|
+
Terminal means: all steps ran, a step failed, the run was aborted, or the total
|
|
1163
|
+
timeout expired. It explicitly does **not** include the pause paths — `stepTo`
|
|
1164
|
+
(detected as `targetEnd < commands.length` with nothing failed), an unexpected
|
|
1165
|
+
breakpoint, or a click-validation failure, which return early and never reach the
|
|
1166
|
+
teardown call. A paused run is not finished, and tearing down would destroy the
|
|
1167
|
+
state the user paused to inspect.
|
|
1168
|
+
|
|
1169
|
+
Three deliberate departures from how nested sequences are otherwise run:
|
|
1170
|
+
|
|
1171
|
+
| Property | Why |
|
|
1172
|
+
|---|---|
|
|
1173
|
+
| Own `teardownTimeout` (default 60s), not `totalTimeout` | The commonest reason a run needs cleanup is that it timed out. Drawing on the exhausted parent budget would skip teardown in exactly that case. |
|
|
1174
|
+
| The run's `AbortSignal` is not forwarded | `replay cancel` must stop the work, not the cleanup. A cancelled run is precisely one that has left something behind. |
|
|
1175
|
+
| Shares `ctx.variableStore` | Teardown revokes what setup minted, and the capturing step may have run long before the failure. |
|
|
1176
|
+
|
|
1177
|
+
The synthetic sequence passed to the nested `executeSteps` call sets
|
|
1178
|
+
`teardown: undefined`; without it the teardown run reaches the same code and runs
|
|
1179
|
+
the teardown again, unboundedly.
|
|
1180
|
+
|
|
1181
|
+
Results land on `ExecutionResult.teardownResults` / `teardownFailed` and are
|
|
1182
|
+
rendered in their own section — never merged into `results` or the
|
|
1183
|
+
successful/failed counts. A broken cleanup must not turn a passing run red, nor
|
|
1184
|
+
make a failing one look like it failed somewhere it did not.
|
|
1185
|
+
|
|
1186
|
+
Teardown is best-effort by construction: a killed cdp-tools process takes any
|
|
1187
|
+
pending teardown with it. It reduces accumulation; it cannot guarantee a clean
|
|
1188
|
+
world, so assertions that depend on absence remain order-dependent regardless.
|
|
1189
|
+
|
|
1190
|
+
## Debug-Aware Replay
|
|
1191
|
+
|
|
1192
|
+
Replay handles debugging sequences specially.
|
|
1193
|
+
|
|
1194
|
+
### Fresh callFrameId Replacement
|
|
1195
|
+
|
|
1196
|
+
When replaying `inspect({ action: 'getVariables' })` with a recorded
|
|
1197
|
+
`callFrameId`, replay fetches the current call stack on that step's connection
|
|
1198
|
+
and swaps in a fresh ID.
|
|
1199
|
+
|
|
1200
|
+
### Auto-Resume
|
|
1201
|
+
|
|
1202
|
+
If the debugger is already paused when a run starts (or when checking an
|
|
1203
|
+
existing connection), replay resumes it first, so a leftover pause from a
|
|
1204
|
+
previous run can't stall the sequence.
|
|
1205
|
+
|
|
1206
|
+
### Expected vs Unexpected Breakpoints
|
|
1207
|
+
|
|
1208
|
+
Replay tracks breakpoints that the sequence itself sets (`breakpoint({ action:
|
|
1209
|
+
'set' })`), including ±1 line to absorb CDP's 0-based/1-based resolution. After
|
|
1210
|
+
each step it checks whether execution is paused:
|
|
1211
|
+
|
|
1212
|
+
- paused at a breakpoint this sequence set → **keep going**;
|
|
1213
|
+
- paused anywhere else → **stop and report the breakpoint hit**, with the pause
|
|
1214
|
+
location and the connection to inspect it on.
|
|
1215
|
+
|
|
1216
|
+
### Debug State Output
|
|
1217
|
+
|
|
1218
|
+
After a run that completed with no failures, if breakpoints are active or
|
|
1219
|
+
execution is paused, replay appends the current debug state:
|
|
1220
|
+
|
|
1221
|
+
```
|
|
1222
|
+
## Debug State
|
|
1223
|
+
|
|
1224
|
+
⏸️ **Execution paused** at http://localhost:3101/client.js:6
|
|
1225
|
+
|
|
1226
|
+
**Next steps:**
|
|
1227
|
+
- Inspect call stack: `inspect({ action: 'getCallStack', connectionReason: '...' })`
|
|
1228
|
+
- Get variables: `inspect({ action: 'getVariables', connectionReason: '...', callFrameId: '<from call stack>' })`
|
|
1229
|
+
- Resume execution: `execution({ action: 'resume', connectionReason: '...' })`
|
|
1230
|
+
- Step over: `execution({ action: 'stepOver', connectionReason: '...' })`
|
|
1231
|
+
|
|
1232
|
+
🔴 **1 active breakpoint**
|
|
1233
|
+
- List breakpoints: `breakpoint({ action: 'list', connectionReason: '...' })`
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
## Step Robustness
|
|
1237
|
+
|
|
1238
|
+
Beyond click validation, each step gets some automatic help:
|
|
1239
|
+
|
|
1240
|
+
- **Retries:** `input` `click` / `type` / `hover` retry up to 5 times, 500ms
|
|
1241
|
+
apart, when the failure looks like "element not found" - enough for a
|
|
1242
|
+
component that hasn't mounted yet.
|
|
1243
|
+
- **Element pre-wait:** after a `navigate` step or a click, if the next step is
|
|
1244
|
+
an `input` with a selector, replay waits for that selector (5 tries, 500ms
|
|
1245
|
+
apart) on the *next step's* connection.
|
|
1246
|
+
- **Navigation validation:** after every `navigate` step, replay checks the page
|
|
1247
|
+
didn't land on `about:blank`, a `chrome-error://` page, an `ERR_*`, or a
|
|
1248
|
+
"site can't be reached" title.
|
|
1249
|
+
- **Port check:** before navigating to a `localhost` URL (including the
|
|
1250
|
+
sequence's `startUrl`), the port is checked and the run fails fast with a
|
|
1251
|
+
clear message rather than loading an error page.
|
|
1252
|
+
- **Typed-text validation:** after `input({ action: 'type', selector })`, the
|
|
1253
|
+
field's value (or `innerText` for contenteditable) is compared against what
|
|
1254
|
+
was typed - exact match, or "ends with" when `append: true`.
|
|
1255
|
+
- **Recorded delays:** delays captured during interaction recording are replayed
|
|
1256
|
+
(capped by `replay.maxDelayMs`, default 1000ms).
|
|
1257
|
+
|
|
1258
|
+
These are best-effort niceties with short, fixed budgets. When a step
|
|
1259
|
+
genuinely depends on async work settling - a page load after `location.href`,
|
|
1260
|
+
a spinner clearing, an async probe writing a global - add an explicit `wait`
|
|
1261
|
+
step instead of relying on them.
|
|
1262
|
+
|
|
1263
|
+
## Explicit Waits (`wait` steps)
|
|
1264
|
+
|
|
1265
|
+
`wait` is a first-class sequence step for "the previous step kicked off async
|
|
1266
|
+
work". Exactly one of four mutually exclusive forms:
|
|
1267
|
+
|
|
1268
|
+
```javascript
|
|
1269
|
+
{ tool: 'wait', params: { selector: 'button:has-text("Join")' } } // element appears
|
|
1270
|
+
{ tool: 'wait', params: { selectorGone: '.spinner' } } // element disappears
|
|
1271
|
+
{ tool: 'wait', params: { expression: 'window.__probe !== "PENDING"' } }
|
|
1272
|
+
{ tool: 'wait', params: { ms: 500 } } // fixed sleep, last resort
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
- The condition forms are polled **from the MCP side** as a synchronous check
|
|
1276
|
+
(default `pollIntervalMs` 100, `timeoutMs` 15000). Because nothing waits
|
|
1277
|
+
inside the page, a wait survives a navigation that happens mid-wait (each
|
|
1278
|
+
poll simply runs in whatever document exists at that moment) and never
|
|
1279
|
+
depends on in-page timers or promises resolving.
|
|
1280
|
+
- `expression` must be synchronous - don't `await` in it. Kick async work off
|
|
1281
|
+
in a prior step, have it write a global, and wait on the global.
|
|
1282
|
+
- On timeout the step returns an error (`WAIT_TIMEOUT`, including the last
|
|
1283
|
+
evaluation error if the predicate was throwing), which stops the sequence
|
|
1284
|
+
like any other failed step. A `wait` never hangs a run.
|
|
1285
|
+
- The run-level `connectionReason` is injected like any other step, and a
|
|
1286
|
+
per-step `connectionReason` is honoured (multi-device sequences).
|
|
1287
|
+
- `wait({ ms })` needs no browser at all and never triggers a Chrome
|
|
1288
|
+
auto-launch; `wait({ expression })` also works against a Node.js target.
|
|
1289
|
+
|
|
1290
|
+
## Click Validation
|
|
1291
|
+
|
|
1292
|
+
Click steps validate their effects. When validation fails, the sequence pauses
|
|
1293
|
+
for inspection rather than failing outright.
|
|
1294
|
+
|
|
1295
|
+
### What Gets Validated
|
|
1296
|
+
|
|
1297
|
+
- **Console errors**: new errors after the click (default: enabled, fail mode `error`)
|
|
1298
|
+
- **Navigation**: if the click caused navigation, that it succeeded (default: enabled)
|
|
1299
|
+
- **DOM mutations**: whether the click changed anything (default: disabled)
|
|
1300
|
+
- **Network requests**: failed POST requests (default: disabled)
|
|
1301
|
+
|
|
1302
|
+
New console warnings and logs are recorded as information only.
|
|
1303
|
+
|
|
1304
|
+
### Pause on Failure
|
|
1305
|
+
|
|
1306
|
+
When validation fails the sequence pauses **on the failed step** - the active
|
|
1307
|
+
sequence's cursor is rewound so the next `step` re-runs it. You can:
|
|
1308
|
+
|
|
1309
|
+
- inspect the error state with `console`, `network`, `dom`, `screenshot`, ...
|
|
1310
|
+
- retry the failed step: `replay({ action: 'step', stepCount: 1 })`
|
|
1311
|
+
- run the rest: `replay({ action: 'finish' })`
|
|
1312
|
+
- abandon it: `replay({ action: 'cancel' })`
|
|
1313
|
+
|
|
1314
|
+
There is no "skip this step" action - `step` always re-runs the step it is
|
|
1315
|
+
parked on.
|
|
1316
|
+
|
|
1317
|
+
### Configuration
|
|
1318
|
+
|
|
1319
|
+
Configure in `.cdp-tools/config.json` (values shown are the defaults):
|
|
1320
|
+
|
|
1321
|
+
```json
|
|
1322
|
+
{
|
|
1323
|
+
"clickValidation": {
|
|
1324
|
+
"enabled": true,
|
|
1325
|
+
"validateNavigation": true,
|
|
1326
|
+
"requireDomChanges": false,
|
|
1327
|
+
"domChangesFailMode": "warn",
|
|
1328
|
+
"failOnConsoleErrors": true,
|
|
1329
|
+
"consoleErrorsFailMode": "error",
|
|
1330
|
+
"validateNetworkPayload": false,
|
|
1331
|
+
"networkFailMode": "warn",
|
|
1332
|
+
"postClickDelayMs": 100
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
**Fail modes:**
|
|
1338
|
+
- `error`: pauses the sequence for inspection
|
|
1339
|
+
- `warn`: logs a warning and continues
|
|
1340
|
+
|
|
1341
|
+
## Use Cases
|
|
1342
|
+
|
|
1343
|
+
### Regression Testing
|
|
1344
|
+
|
|
1345
|
+
```javascript
|
|
1346
|
+
// Record interactions directly - this call blocks until you click ✓ in the browser
|
|
1347
|
+
launchChrome({ reference: "checkout-test" })
|
|
1348
|
+
replay({ action: 'recordInteraction', connectionReason: 'checkout-test' })
|
|
1349
|
+
|
|
1350
|
+
// Export as Playwright test
|
|
1351
|
+
replay({ action: 'export', name: 'checkout-test', format: 'playwright' })
|
|
1352
|
+
|
|
1353
|
+
// Run anytime to verify
|
|
1354
|
+
replay({ action: 'run', name: 'checkout-test', connectionReason: 'test-run' })
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
### Debugging Workflows
|
|
1358
|
+
|
|
1359
|
+
```javascript
|
|
1360
|
+
// Create a debug sequence from what you just did
|
|
1361
|
+
replay({
|
|
1362
|
+
action: 'create',
|
|
1363
|
+
name: 'debug-auth-bug',
|
|
1364
|
+
description: 'Sets breakpoint on auth handler and triggers login',
|
|
1365
|
+
expectedOutcome: 'Debugger pauses at auth.js:42 showing user object',
|
|
1366
|
+
indices: [0, 1, 2, 3, 4, 5]
|
|
1367
|
+
})
|
|
1368
|
+
|
|
1369
|
+
// Run to debug - debug state is shown automatically after the run
|
|
1370
|
+
replay({ action: 'run', name: 'debug-auth-bug' })
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
### Verifying the Same Flow on Two Deployments
|
|
1374
|
+
|
|
1375
|
+
```javascript
|
|
1376
|
+
replay({ action: 'run', name: 'checkout', baseUrl: 'http://localhost:3000' })
|
|
1377
|
+
replay({ action: 'run', name: 'checkout', baseUrl: 'https://staging.example.com' })
|
|
1378
|
+
```
|
|
1379
|
+
|
|
1380
|
+
### Cross-Device Handoff
|
|
1381
|
+
|
|
1382
|
+
Hand-author steps with per-step `connectionReason` and capture the handoff value
|
|
1383
|
+
with `saveAs` - see [Per-Step Connections](#per-step-connections-multi-device-sequences).
|
|
1384
|
+
|
|
1385
|
+
### Automation
|
|
1386
|
+
|
|
1387
|
+
```javascript
|
|
1388
|
+
replay({
|
|
1389
|
+
action: 'create',
|
|
1390
|
+
name: 'daily-smoke-test',
|
|
1391
|
+
description: 'Navigates key pages and checks for console errors',
|
|
1392
|
+
expectedOutcome: 'All pages load without errors',
|
|
1393
|
+
indices: [0, 1, 2, 3, 4, 5, 6, 7]
|
|
1394
|
+
})
|
|
1395
|
+
|
|
1396
|
+
replay({ action: 'run', name: 'daily-smoke-test', killChromeOnFinish: true })
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
## Notes
|
|
1400
|
+
|
|
1401
|
+
- **Recording:** only tool calls are recorded, not responses.
|
|
1402
|
+
- **Replay:** steps execute sequentially, and the run stops at the first failing
|
|
1403
|
+
step.
|
|
1404
|
+
- **Persistence:** in-memory sequences are cleared on restart; use
|
|
1405
|
+
`export`/`load` for disk persistence. `run` and `get` load from disk by name
|
|
1406
|
+
automatically.
|
|
1407
|
+
- **Connection stripping:** `connectionReason` is removed from commands as they
|
|
1408
|
+
are recorded into history, for portability.
|
|
1409
|
+
- **Validation timing:** tool *names* are validated at `create`/`load` time;
|
|
1410
|
+
`run` does not re-validate, so a sequence edited on disk by hand is best
|
|
1411
|
+
round-tripped through `load` before running.
|