tabwright 1.0.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/bin.js +4 -0
- package/dist/a11y-client.js +204 -0
- package/dist/agent-skills/tabwright/SKILL.md +119 -0
- package/dist/aria-snapshot.d.ts +262 -0
- package/dist/aria-snapshot.d.ts.map +1 -0
- package/dist/aria-snapshot.js +1256 -0
- package/dist/aria-snapshot.js.map +1 -0
- package/dist/aria-snapshot.test.d.ts +2 -0
- package/dist/aria-snapshot.test.d.ts.map +1 -0
- package/dist/aria-snapshot.test.js +270 -0
- package/dist/aria-snapshot.test.js.map +1 -0
- package/dist/aria-snapshot.unit.test.d.ts +2 -0
- package/dist/aria-snapshot.unit.test.d.ts.map +1 -0
- package/dist/aria-snapshot.unit.test.js +542 -0
- package/dist/aria-snapshot.unit.test.js.map +1 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
- package/dist/bippy.js +894 -0
- package/dist/browser-config.d.ts +13 -0
- package/dist/browser-config.d.ts.map +1 -0
- package/dist/browser-config.js +126 -0
- package/dist/browser-config.js.map +1 -0
- package/dist/browser-install.d.ts +16 -0
- package/dist/browser-install.d.ts.map +1 -0
- package/dist/browser-install.js +238 -0
- package/dist/browser-install.js.map +1 -0
- package/dist/browser-launch.d.ts +17 -0
- package/dist/browser-launch.d.ts.map +1 -0
- package/dist/browser-launch.js +44 -0
- package/dist/browser-launch.js.map +1 -0
- package/dist/builtin-capabilities.d.ts +28 -0
- package/dist/builtin-capabilities.d.ts.map +1 -0
- package/dist/builtin-capabilities.js +2673 -0
- package/dist/builtin-capabilities.js.map +1 -0
- package/dist/capability-agent-skill.d.ts +60 -0
- package/dist/capability-agent-skill.d.ts.map +1 -0
- package/dist/capability-agent-skill.js +225 -0
- package/dist/capability-agent-skill.js.map +1 -0
- package/dist/capability-auth-state.d.ts +34 -0
- package/dist/capability-auth-state.d.ts.map +1 -0
- package/dist/capability-auth-state.js +157 -0
- package/dist/capability-auth-state.js.map +1 -0
- package/dist/capability-auth.d.ts +36 -0
- package/dist/capability-auth.d.ts.map +1 -0
- package/dist/capability-auth.js +198 -0
- package/dist/capability-auth.js.map +1 -0
- package/dist/capability-cli-confirmation.test.d.ts +2 -0
- package/dist/capability-cli-confirmation.test.d.ts.map +1 -0
- package/dist/capability-cli-confirmation.test.js +79 -0
- package/dist/capability-cli-confirmation.test.js.map +1 -0
- package/dist/capability-options.d.ts +20 -0
- package/dist/capability-options.d.ts.map +1 -0
- package/dist/capability-options.js +31 -0
- package/dist/capability-options.js.map +1 -0
- package/dist/capability-options.test.d.ts +2 -0
- package/dist/capability-options.test.d.ts.map +1 -0
- package/dist/capability-options.test.js +81 -0
- package/dist/capability-options.test.js.map +1 -0
- package/dist/capability-package.d.ts +27 -0
- package/dist/capability-package.d.ts.map +1 -0
- package/dist/capability-package.js +444 -0
- package/dist/capability-package.js.map +1 -0
- package/dist/capability-package.test.d.ts +2 -0
- package/dist/capability-package.test.d.ts.map +1 -0
- package/dist/capability-package.test.js +179 -0
- package/dist/capability-package.test.js.map +1 -0
- package/dist/capability-registry.d.ts +262 -0
- package/dist/capability-registry.d.ts.map +1 -0
- package/dist/capability-registry.js +931 -0
- package/dist/capability-registry.js.map +1 -0
- package/dist/capability-registry.test.d.ts +2 -0
- package/dist/capability-registry.test.d.ts.map +1 -0
- package/dist/capability-registry.test.js +1683 -0
- package/dist/capability-registry.test.js.map +1 -0
- package/dist/capability-runner.d.ts +99 -0
- package/dist/capability-runner.d.ts.map +1 -0
- package/dist/capability-runner.js +608 -0
- package/dist/capability-runner.js.map +1 -0
- package/dist/capability-search-relevance.test.d.ts +2 -0
- package/dist/capability-search-relevance.test.d.ts.map +1 -0
- package/dist/capability-search-relevance.test.js +67 -0
- package/dist/capability-search-relevance.test.js.map +1 -0
- package/dist/capability-studio.d.ts +11 -0
- package/dist/capability-studio.d.ts.map +1 -0
- package/dist/capability-studio.js +593 -0
- package/dist/capability-studio.js.map +1 -0
- package/dist/capability-studio.test.d.ts +2 -0
- package/dist/capability-studio.test.d.ts.map +1 -0
- package/dist/capability-studio.test.js +41 -0
- package/dist/capability-studio.test.js.map +1 -0
- package/dist/cdp-log.d.ts +19 -0
- package/dist/cdp-log.d.ts.map +1 -0
- package/dist/cdp-log.js +83 -0
- package/dist/cdp-log.js.map +1 -0
- package/dist/cdp-log.test.d.ts +2 -0
- package/dist/cdp-log.test.d.ts.map +1 -0
- package/dist/cdp-log.test.js +109 -0
- package/dist/cdp-log.test.js.map +1 -0
- package/dist/cdp-relay.d.ts +19 -0
- package/dist/cdp-relay.d.ts.map +1 -0
- package/dist/cdp-relay.js +2375 -0
- package/dist/cdp-relay.js.map +1 -0
- package/dist/cdp-session.d.ts +37 -0
- package/dist/cdp-session.d.ts.map +1 -0
- package/dist/cdp-session.js +36 -0
- package/dist/cdp-session.js.map +1 -0
- package/dist/cdp-types.d.ts +65 -0
- package/dist/cdp-types.d.ts.map +1 -0
- package/dist/cdp-types.js +91 -0
- package/dist/cdp-types.js.map +1 -0
- package/dist/channel-owner-inspect.test.d.ts +2 -0
- package/dist/channel-owner-inspect.test.d.ts.map +1 -0
- package/dist/channel-owner-inspect.test.js +75 -0
- package/dist/channel-owner-inspect.test.js.map +1 -0
- package/dist/chrome-discovery.d.ts +65 -0
- package/dist/chrome-discovery.d.ts.map +1 -0
- package/dist/chrome-discovery.js +173 -0
- package/dist/chrome-discovery.js.map +1 -0
- package/dist/chrome-discovery.test.d.ts +2 -0
- package/dist/chrome-discovery.test.d.ts.map +1 -0
- package/dist/chrome-discovery.test.js +41 -0
- package/dist/chrome-discovery.test.js.map +1 -0
- package/dist/clean-html.d.ts +11 -0
- package/dist/clean-html.d.ts.map +1 -0
- package/dist/clean-html.js +106 -0
- package/dist/clean-html.js.map +1 -0
- package/dist/cli-help.test.d.ts +2 -0
- package/dist/cli-help.test.d.ts.map +1 -0
- package/dist/cli-help.test.js +98 -0
- package/dist/cli-help.test.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2709 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloud-client.d.ts +56 -0
- package/dist/cloud-client.d.ts.map +1 -0
- package/dist/cloud-client.js +127 -0
- package/dist/cloud-client.js.map +1 -0
- package/dist/create-logger.d.ts +9 -0
- package/dist/create-logger.d.ts.map +1 -0
- package/dist/create-logger.js +27 -0
- package/dist/create-logger.js.map +1 -0
- package/dist/debugger-api.md +458 -0
- package/dist/debugger-examples-types.d.ts +24 -0
- package/dist/debugger-examples-types.d.ts.map +1 -0
- package/dist/debugger-examples-types.js +2 -0
- package/dist/debugger-examples-types.js.map +1 -0
- package/dist/debugger-examples.d.ts +6 -0
- package/dist/debugger-examples.d.ts.map +1 -0
- package/dist/debugger-examples.js +53 -0
- package/dist/debugger-examples.js.map +1 -0
- package/dist/debugger.d.ts +381 -0
- package/dist/debugger.d.ts.map +1 -0
- package/dist/debugger.js +630 -0
- package/dist/debugger.js.map +1 -0
- package/dist/diff-utils.d.ts +20 -0
- package/dist/diff-utils.d.ts.map +1 -0
- package/dist/diff-utils.js +50 -0
- package/dist/diff-utils.js.map +1 -0
- package/dist/diff-utils.test.d.ts +2 -0
- package/dist/diff-utils.test.d.ts.map +1 -0
- package/dist/diff-utils.test.js +137 -0
- package/dist/diff-utils.test.js.map +1 -0
- package/dist/doctor.d.ts +44 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +246 -0
- package/dist/doctor.js.map +1 -0
- package/dist/doctor.test.d.ts +2 -0
- package/dist/doctor.test.d.ts.map +1 -0
- package/dist/doctor.test.js +208 -0
- package/dist/doctor.test.js.map +1 -0
- package/dist/editor-api.md +374 -0
- package/dist/editor-examples.d.ts +11 -0
- package/dist/editor-examples.d.ts.map +1 -0
- package/dist/editor-examples.js +124 -0
- package/dist/editor-examples.js.map +1 -0
- package/dist/editor.d.ts +203 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +363 -0
- package/dist/editor.js.map +1 -0
- package/dist/env-compat.d.ts +2 -0
- package/dist/env-compat.d.ts.map +1 -0
- package/dist/env-compat.js +27 -0
- package/dist/env-compat.js.map +1 -0
- package/dist/executor.d.ts +229 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +1465 -0
- package/dist/executor.js.map +1 -0
- package/dist/executor.unit.test.d.ts +2 -0
- package/dist/executor.unit.test.d.ts.map +1 -0
- package/dist/executor.unit.test.js +143 -0
- package/dist/executor.unit.test.js.map +1 -0
- package/dist/extension/_locales/en/messages.json +278 -0
- package/dist/extension/_locales/zh_CN/messages.json +256 -0
- package/dist/extension/assets/options-CYH96Lcx.css +1349 -0
- package/dist/extension/background.js +3994 -0
- package/dist/extension/icons/SolarCursorSquareBold.png +0 -0
- package/dist/extension/icons/icon-black-128.png +0 -0
- package/dist/extension/icons/icon-black-16.png +0 -0
- package/dist/extension/icons/icon-black-32.png +0 -0
- package/dist/extension/icons/icon-black-48.png +0 -0
- package/dist/extension/icons/icon-gray-128.png +0 -0
- package/dist/extension/icons/icon-gray-16.png +0 -0
- package/dist/extension/icons/icon-gray-32.png +0 -0
- package/dist/extension/icons/icon-gray-48.png +0 -0
- package/dist/extension/icons/icon-green-128.png +0 -0
- package/dist/extension/icons/icon-green-16.png +0 -0
- package/dist/extension/icons/icon-green-32.png +0 -0
- package/dist/extension/icons/icon-green-48.png +0 -0
- package/dist/extension/icons/tabwright-icon-black.png +0 -0
- package/dist/extension/icons/tabwright-icon-gray-disabled.png +0 -0
- package/dist/extension/icons/tabwright-icon-gray-disabled.svg +1 -0
- package/dist/extension/manifest.json +54 -0
- package/dist/extension/options.js +16145 -0
- package/dist/extension/rrweb-recorder.js +12952 -0
- package/dist/extension/src/options.html +165 -0
- package/dist/extension/src/prism-bash.min.js +1 -0
- package/dist/extension/src/prism.min.js +1 -0
- package/dist/extension/src/welcome.html +396 -0
- package/dist/extension-connection.test.d.ts +2 -0
- package/dist/extension-connection.test.d.ts.map +1 -0
- package/dist/extension-connection.test.js +708 -0
- package/dist/extension-connection.test.js.map +1 -0
- package/dist/extension-icon-warning.test.d.ts +2 -0
- package/dist/extension-icon-warning.test.d.ts.map +1 -0
- package/dist/extension-icon-warning.test.js +33 -0
- package/dist/extension-icon-warning.test.js.map +1 -0
- package/dist/ghost-browser.d.ts +62 -0
- package/dist/ghost-browser.d.ts.map +1 -0
- package/dist/ghost-browser.js +107 -0
- package/dist/ghost-browser.js.map +1 -0
- package/dist/ghost-cursor-client.js +374 -0
- package/dist/ghost-cursor-controller.d.ts +46 -0
- package/dist/ghost-cursor-controller.d.ts.map +1 -0
- package/dist/ghost-cursor-controller.js +98 -0
- package/dist/ghost-cursor-controller.js.map +1 -0
- package/dist/ghost-cursor.d.ts +27 -0
- package/dist/ghost-cursor.d.ts.map +1 -0
- package/dist/ghost-cursor.js +79 -0
- package/dist/ghost-cursor.js.map +1 -0
- package/dist/htmlrewrite.d.ts +8 -0
- package/dist/htmlrewrite.d.ts.map +1 -0
- package/dist/htmlrewrite.js +339 -0
- package/dist/htmlrewrite.js.map +1 -0
- package/dist/htmlrewrite.test.d.ts +2 -0
- package/dist/htmlrewrite.test.d.ts.map +1 -0
- package/dist/htmlrewrite.test.js +7975 -0
- package/dist/htmlrewrite.test.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/kill-port.d.ts +29 -0
- package/dist/kill-port.d.ts.map +1 -0
- package/dist/kill-port.js +190 -0
- package/dist/kill-port.js.map +1 -0
- package/dist/kitty-graphics.d.ts +22 -0
- package/dist/kitty-graphics.d.ts.map +1 -0
- package/dist/kitty-graphics.js +71 -0
- package/dist/kitty-graphics.js.map +1 -0
- package/dist/kitty-graphics.test.d.ts +2 -0
- package/dist/kitty-graphics.test.d.ts.map +1 -0
- package/dist/kitty-graphics.test.js +125 -0
- package/dist/kitty-graphics.test.js.map +1 -0
- package/dist/locator-selector.test.d.ts +2 -0
- package/dist/locator-selector.test.d.ts.map +1 -0
- package/dist/locator-selector.test.js +98 -0
- package/dist/locator-selector.test.js.map +1 -0
- package/dist/mcp-client.d.ts +20 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +56 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/mcp.d.ts +6 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +511 -0
- package/dist/mcp.js.map +1 -0
- package/dist/on-mouse-action.test.d.ts +2 -0
- package/dist/on-mouse-action.test.d.ts.map +1 -0
- package/dist/on-mouse-action.test.js +180 -0
- package/dist/on-mouse-action.test.js.map +1 -0
- package/dist/options-page.test.d.ts +2 -0
- package/dist/options-page.test.d.ts.map +1 -0
- package/dist/options-page.test.js +773 -0
- package/dist/options-page.test.js.map +1 -0
- package/dist/package-paths.d.ts +4 -0
- package/dist/package-paths.d.ts.map +1 -0
- package/dist/package-paths.js +30 -0
- package/dist/package-paths.js.map +1 -0
- package/dist/page-markdown.d.ts +40 -0
- package/dist/page-markdown.d.ts.map +1 -0
- package/dist/page-markdown.js +181 -0
- package/dist/page-markdown.js.map +1 -0
- package/dist/performance-examples.d.ts +5 -0
- package/dist/performance-examples.d.ts.map +1 -0
- package/dist/performance-examples.js +112 -0
- package/dist/performance-examples.js.map +1 -0
- package/dist/performance-profiling.md +417 -0
- package/dist/playwright-import.d.ts +19 -0
- package/dist/playwright-import.d.ts.map +1 -0
- package/dist/playwright-import.js +40 -0
- package/dist/playwright-import.js.map +1 -0
- package/dist/popup-relocation.test.d.ts +7 -0
- package/dist/popup-relocation.test.d.ts.map +1 -0
- package/dist/popup-relocation.test.js +139 -0
- package/dist/popup-relocation.test.js.map +1 -0
- package/dist/product-paths.d.ts +9 -0
- package/dist/product-paths.d.ts.map +1 -0
- package/dist/product-paths.js +24 -0
- package/dist/product-paths.js.map +1 -0
- package/dist/product-paths.test.d.ts +2 -0
- package/dist/product-paths.test.d.ts.map +1 -0
- package/dist/product-paths.test.js +39 -0
- package/dist/product-paths.test.js.map +1 -0
- package/dist/prompt.md +1004 -0
- package/dist/protocol.d.ts +248 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +59 -0
- package/dist/protocol.js.map +1 -0
- package/dist/protocol.test.d.ts +2 -0
- package/dist/protocol.test.d.ts.map +1 -0
- package/dist/protocol.test.js +31 -0
- package/dist/protocol.test.js.map +1 -0
- package/dist/react-source.d.ts +57 -0
- package/dist/react-source.d.ts.map +1 -0
- package/dist/react-source.js +254 -0
- package/dist/react-source.js.map +1 -0
- package/dist/readability.js +1674 -0
- package/dist/relay-client.d.ts +104 -0
- package/dist/relay-client.d.ts.map +1 -0
- package/dist/relay-client.js +426 -0
- package/dist/relay-client.js.map +1 -0
- package/dist/relay-client.test.d.ts +2 -0
- package/dist/relay-client.test.d.ts.map +1 -0
- package/dist/relay-client.test.js +305 -0
- package/dist/relay-client.test.js.map +1 -0
- package/dist/relay-core.test.d.ts +2 -0
- package/dist/relay-core.test.d.ts.map +1 -0
- package/dist/relay-core.test.js +1427 -0
- package/dist/relay-core.test.js.map +1 -0
- package/dist/relay-navigation.test.d.ts +2 -0
- package/dist/relay-navigation.test.d.ts.map +1 -0
- package/dist/relay-navigation.test.js +661 -0
- package/dist/relay-navigation.test.js.map +1 -0
- package/dist/relay-session.test.d.ts +2 -0
- package/dist/relay-session.test.d.ts.map +1 -0
- package/dist/relay-session.test.js +1121 -0
- package/dist/relay-session.test.js.map +1 -0
- package/dist/relay-state.d.ts +178 -0
- package/dist/relay-state.d.ts.map +1 -0
- package/dist/relay-state.js +346 -0
- package/dist/relay-state.js.map +1 -0
- package/dist/relay-state.test.d.ts +2 -0
- package/dist/relay-state.test.d.ts.map +1 -0
- package/dist/relay-state.test.js +594 -0
- package/dist/relay-state.test.js.map +1 -0
- package/dist/replay-ai-index.d.ts +99 -0
- package/dist/replay-ai-index.d.ts.map +1 -0
- package/dist/replay-ai-index.js +677 -0
- package/dist/replay-ai-index.js.map +1 -0
- package/dist/replay-ai-index.test.d.ts +2 -0
- package/dist/replay-ai-index.test.d.ts.map +1 -0
- package/dist/replay-ai-index.test.js +255 -0
- package/dist/replay-ai-index.test.js.map +1 -0
- package/dist/replay-cli.test.d.ts +2 -0
- package/dist/replay-cli.test.d.ts.map +1 -0
- package/dist/replay-cli.test.js +275 -0
- package/dist/replay-cli.test.js.map +1 -0
- package/dist/replay-eval.d.ts +44 -0
- package/dist/replay-eval.d.ts.map +1 -0
- package/dist/replay-eval.js +902 -0
- package/dist/replay-eval.js.map +1 -0
- package/dist/replay-handoff.d.ts +29 -0
- package/dist/replay-handoff.d.ts.map +1 -0
- package/dist/replay-handoff.js +83 -0
- package/dist/replay-handoff.js.map +1 -0
- package/dist/replay-handoff.test.d.ts +2 -0
- package/dist/replay-handoff.test.d.ts.map +1 -0
- package/dist/replay-handoff.test.js +123 -0
- package/dist/replay-handoff.test.js.map +1 -0
- package/dist/replay-workflow-compiler.d.ts +44 -0
- package/dist/replay-workflow-compiler.d.ts.map +1 -0
- package/dist/replay-workflow-compiler.js +601 -0
- package/dist/replay-workflow-compiler.js.map +1 -0
- package/dist/replay-workflow-compiler.test.d.ts +2 -0
- package/dist/replay-workflow-compiler.test.d.ts.map +1 -0
- package/dist/replay-workflow-compiler.test.js +223 -0
- package/dist/replay-workflow-compiler.test.js.map +1 -0
- package/dist/rrweb-recording-relay.d.ts +56 -0
- package/dist/rrweb-recording-relay.d.ts.map +1 -0
- package/dist/rrweb-recording-relay.js +299 -0
- package/dist/rrweb-recording-relay.js.map +1 -0
- package/dist/rrweb-recording-relay.test.d.ts +2 -0
- package/dist/rrweb-recording-relay.test.d.ts.map +1 -0
- package/dist/rrweb-recording-relay.test.js +85 -0
- package/dist/rrweb-recording-relay.test.js.map +1 -0
- package/dist/rrweb-recording.d.ts +104 -0
- package/dist/rrweb-recording.d.ts.map +1 -0
- package/dist/rrweb-recording.js +180 -0
- package/dist/rrweb-recording.js.map +1 -0
- package/dist/scoped-fs.d.ts +95 -0
- package/dist/scoped-fs.d.ts.map +1 -0
- package/dist/scoped-fs.js +358 -0
- package/dist/scoped-fs.js.map +1 -0
- package/dist/scoped-fs.test.d.ts +5 -0
- package/dist/scoped-fs.test.d.ts.map +1 -0
- package/dist/scoped-fs.test.js +73 -0
- package/dist/scoped-fs.test.js.map +1 -0
- package/dist/selector-generator.js +7378 -0
- package/dist/skill-stub.test.d.ts +2 -0
- package/dist/skill-stub.test.d.ts.map +1 -0
- package/dist/skill-stub.test.js +22 -0
- package/dist/skill-stub.test.js.map +1 -0
- package/dist/snapshot-tools.test.d.ts +2 -0
- package/dist/snapshot-tools.test.d.ts.map +1 -0
- package/dist/snapshot-tools.test.js +856 -0
- package/dist/snapshot-tools.test.js.map +1 -0
- package/dist/start-relay-server.d.ts +6 -0
- package/dist/start-relay-server.d.ts.map +1 -0
- package/dist/start-relay-server.js +57 -0
- package/dist/start-relay-server.js.map +1 -0
- package/dist/styles-api.md +124 -0
- package/dist/styles-examples.d.ts +8 -0
- package/dist/styles-examples.d.ts.map +1 -0
- package/dist/styles-examples.js +64 -0
- package/dist/styles-examples.js.map +1 -0
- package/dist/styles.d.ts +27 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +231 -0
- package/dist/styles.js.map +1 -0
- package/dist/tabwright-agent-skill.d.ts +30 -0
- package/dist/tabwright-agent-skill.d.ts.map +1 -0
- package/dist/tabwright-agent-skill.js +74 -0
- package/dist/tabwright-agent-skill.js.map +1 -0
- package/dist/tabwright-agent-skill.test.d.ts +2 -0
- package/dist/tabwright-agent-skill.test.d.ts.map +1 -0
- package/dist/tabwright-agent-skill.test.js +51 -0
- package/dist/tabwright-agent-skill.test.js.map +1 -0
- package/dist/test-declarations.d.ts +13 -0
- package/dist/test-declarations.d.ts.map +1 -0
- package/dist/test-declarations.js +2 -0
- package/dist/test-declarations.js.map +1 -0
- package/dist/test-utils.d.ts +69 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +448 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/test-utils.test.d.ts +2 -0
- package/dist/test-utils.test.d.ts.map +1 -0
- package/dist/test-utils.test.js +55 -0
- package/dist/test-utils.test.js.map +1 -0
- package/dist/utils.d.ts +26 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +60 -0
- package/dist/utils.js.map +1 -0
- package/dist/wait-for-page-load.d.ts +16 -0
- package/dist/wait-for-page-load.d.ts.map +1 -0
- package/dist/wait-for-page-load.js +146 -0
- package/dist/wait-for-page-load.js.map +1 -0
- package/dist/workflow-capability.d.ts +72 -0
- package/dist/workflow-capability.d.ts.map +1 -0
- package/dist/workflow-capability.js +316 -0
- package/dist/workflow-capability.js.map +1 -0
- package/dist/workflow-script-flow.test.d.ts +2 -0
- package/dist/workflow-script-flow.test.d.ts.map +1 -0
- package/dist/workflow-script-flow.test.js +285 -0
- package/dist/workflow-script-flow.test.js.map +1 -0
- package/package.json +80 -0
- package/src/__snapshots__/x.com.processed.html +1003 -0
- package/src/__snapshots__/x.com.processed.withStyles.html +2720 -0
- package/src/a11y-client.ts +241 -0
- package/src/agent-skills/conan-config/SKILL.md +141 -0
- package/src/agent-skills/conan-config/agents/openai.yaml +4 -0
- package/src/aria-snapshot.test.ts +311 -0
- package/src/aria-snapshot.ts +1681 -0
- package/src/aria-snapshot.unit.test.ts +615 -0
- package/src/aria-snapshots/github-interactive.txt +193 -0
- package/src/aria-snapshots/github-raw.txt +309 -0
- package/src/aria-snapshots/hackernews-interactive.txt +477 -0
- package/src/aria-snapshots/hackernews-raw.txt +428 -0
- package/src/aria-snapshots/prosemirror-interactive.txt +66 -0
- package/src/aria-snapshots/prosemirror-raw.txt +145 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/assets/aria-labels-old-reddit.png +0 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
- package/src/assets/x.com.html +32946 -0
- package/src/browser-config.ts +179 -0
- package/src/browser-install.ts +284 -0
- package/src/browser-launch.ts +75 -0
- package/src/builtin-capabilities.ts +2765 -0
- package/src/capability-agent-skill.ts +305 -0
- package/src/capability-auth-state.ts +197 -0
- package/src/capability-auth.ts +249 -0
- package/src/capability-cli-confirmation.test.ts +90 -0
- package/src/capability-options.test.ts +89 -0
- package/src/capability-options.ts +57 -0
- package/src/capability-package.test.ts +204 -0
- package/src/capability-package.ts +555 -0
- package/src/capability-registry.test.ts +1905 -0
- package/src/capability-registry.ts +1234 -0
- package/src/capability-runner.ts +801 -0
- package/src/capability-search-relevance.test.ts +73 -0
- package/src/capability-studio.test.ts +45 -0
- package/src/capability-studio.ts +641 -0
- package/src/cdp-log.test.ts +131 -0
- package/src/cdp-log.ts +112 -0
- package/src/cdp-relay.ts +2944 -0
- package/src/cdp-session.ts +77 -0
- package/src/cdp-types.ts +158 -0
- package/src/channel-owner-inspect.test.ts +94 -0
- package/src/chrome-discovery.test.ts +50 -0
- package/src/chrome-discovery.ts +220 -0
- package/src/clean-html.ts +141 -0
- package/src/cli-help.test.ts +123 -0
- package/src/cli.ts +3181 -0
- package/src/cloud-client.ts +179 -0
- package/src/create-logger.ts +38 -0
- package/src/debugger-examples-types.ts +16 -0
- package/src/debugger-examples.ts +66 -0
- package/src/debugger.ts +708 -0
- package/src/diff-utils.test.ts +152 -0
- package/src/diff-utils.ts +70 -0
- package/src/doctor.test.ts +244 -0
- package/src/doctor.ts +321 -0
- package/src/editor-examples.ts +158 -0
- package/src/editor.ts +426 -0
- package/src/env-compat.ts +28 -0
- package/src/executor.ts +1824 -0
- package/src/executor.unit.test.ts +168 -0
- package/src/extension-connection.test.ts +855 -0
- package/src/extension-icon-warning.test.ts +45 -0
- package/src/ghost-browser.ts +149 -0
- package/src/ghost-cursor-client.ts +494 -0
- package/src/ghost-cursor-controller.ts +129 -0
- package/src/ghost-cursor.ts +123 -0
- package/src/htmlrewrite.test.ts +8008 -0
- package/src/htmlrewrite.ts +390 -0
- package/src/index.ts +53 -0
- package/src/kill-port.ts +219 -0
- package/src/kitty-graphics.test.ts +139 -0
- package/src/kitty-graphics.ts +79 -0
- package/src/locator-selector.test.ts +119 -0
- package/src/mcp-client.ts +78 -0
- package/src/mcp.ts +629 -0
- package/src/on-mouse-action.test.ts +226 -0
- package/src/options-page.test.ts +839 -0
- package/src/package-paths.ts +38 -0
- package/src/page-markdown.ts +240 -0
- package/src/performance-examples.ts +186 -0
- package/src/playwright-import.ts +59 -0
- package/src/popup-relocation.test.ts +163 -0
- package/src/product-paths.test.ts +47 -0
- package/src/product-paths.ts +28 -0
- package/src/protocol.test.ts +52 -0
- package/src/protocol.ts +351 -0
- package/src/react-source.ts +379 -0
- package/src/relay-client.test.ts +375 -0
- package/src/relay-client.ts +572 -0
- package/src/relay-core.test.ts +1603 -0
- package/src/relay-navigation.test.ts +790 -0
- package/src/relay-session.test.ts +1375 -0
- package/src/relay-state.test.ts +729 -0
- package/src/relay-state.ts +561 -0
- package/src/replay-ai-index.test.ts +278 -0
- package/src/replay-ai-index.ts +872 -0
- package/src/replay-cli.test.ts +325 -0
- package/src/replay-eval.ts +1039 -0
- package/src/replay-handoff.test.ts +163 -0
- package/src/replay-handoff.ts +109 -0
- package/src/replay-workflow-compiler.test.ts +250 -0
- package/src/replay-workflow-compiler.ts +700 -0
- package/src/resource.md +409 -0
- package/src/rrweb-recording-relay.test.ts +99 -0
- package/src/rrweb-recording-relay.ts +400 -0
- package/src/rrweb-recording.ts +322 -0
- package/src/scoped-fs.test.ts +80 -0
- package/src/scoped-fs.ts +420 -0
- package/src/skill-stub.test.ts +24 -0
- package/src/skill.md +1413 -0
- package/src/snapshot-tools.test.ts +1017 -0
- package/src/snapshots/hacker-news-accessibility-full.md +81 -0
- package/src/snapshots/hacker-news-accessibility-interactive.md +43 -0
- package/src/snapshots/page-markdown-output.txt +16 -0
- package/src/snapshots/shadcn-ui-accessibility-full.md +201 -0
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +109 -0
- package/src/start-relay-server.ts +69 -0
- package/src/styles-examples.ts +84 -0
- package/src/styles.ts +343 -0
- package/src/tabwright-agent-skill.test.ts +61 -0
- package/src/tabwright-agent-skill.ts +105 -0
- package/src/test-declarations.ts +13 -0
- package/src/test-utils.test.ts +70 -0
- package/src/test-utils.ts +566 -0
- package/src/utils.ts +78 -0
- package/src/wait-for-page-load.ts +198 -0
- package/src/workflow-capability.ts +417 -0
- package/src/workflow-script-flow.test.ts +388 -0
package/src/skill.md
ADDED
|
@@ -0,0 +1,1413 @@
|
|
|
1
|
+
## CLI Usage
|
|
2
|
+
|
|
3
|
+
This file is the extended reference. The installed Tabwright skill contains the required compact browser protocol; agents should not load this entire reference before every task. Query only the relevant topic when needed, for example `tabwright skill | rg -n -C 20 'working with pages|snapshot|iframe'` on macOS/Linux or `tabwright skill | Select-String -Pattern 'working with pages|snapshot|iframe' -Context 20,20` in Windows PowerShell.
|
|
4
|
+
|
|
5
|
+
If `tabwright` command is not found, install globally or use npx/bunx:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g tabwright@latest
|
|
9
|
+
# or use without installing:
|
|
10
|
+
npx tabwright@latest session new
|
|
11
|
+
bunx tabwright@latest session new
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
If using npx or bunx always use @latest for the first session command. so we are sure of using the latest version of the package
|
|
15
|
+
|
|
16
|
+
Install the agent skill bundled with the same CLI build. Do not fetch it from a separate repository, because fork-specific behavior may differ:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
tabwright skill status --target codex
|
|
20
|
+
tabwright skill install --target codex
|
|
21
|
+
# after a CLI upgrade, use --force only when status reports an outdated installed copy
|
|
22
|
+
tabwright skill install --target codex --force
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The install command writes to `$CODEX_HOME/skills/tabwright/SKILL.md`, or `~/.codex/skills/tabwright/SKILL.md` when `CODEX_HOME` is unset. Open a new agent thread after installation. `tabwright doctor` reports a missing or outdated Tabwright skill and prints the exact repair command.
|
|
26
|
+
|
|
27
|
+
### Capability routing shortcuts
|
|
28
|
+
|
|
29
|
+
For concrete user tasks, first check saved capability contracts. When a capability or one of its operations has `routingHint: "exact-match-direct-run"`, its `match` patterns exactly fit the task, and that operation's `autonomousInvocation.allowed` is true (`trusted`, `sideEffect: "read"`, `requiresConfirmation: false`), run it directly with the extracted input. Routed operation inputs include `action`. Do not search, describe, or open a page first.
|
|
30
|
+
|
|
31
|
+
If the exact-match capability index is not already visible in the current skill or tool context, use `tabwright capability route "<task-or-url>" --json` as the metadata check. Do not use `capability search` or `capability describe` for an exact-match direct-run candidate.
|
|
32
|
+
|
|
33
|
+
When using route output, run the returned `shellCommand` exactly. Do not infer a shell command from the capability id; capability ids are not shell commands.
|
|
34
|
+
|
|
35
|
+
In Codex sandboxed environments, `tabwright capability run ...` writes the capability run log and may write artifacts under `~/.tabwright/capabilities/...`. Run trusted capability commands with escalated/unsandboxed permissions immediately instead of trying once in the sandbox and then retrying. `tabwright capability route ... --json` is metadata-only and can run sandboxed.
|
|
36
|
+
|
|
37
|
+
Do not treat every URL as a direct-run signal. This shortcut only applies to exact capability matches, and it also applies to non-URL tasks when a capability's `match` patterns are specific enough. Capability-specific usage and display rules belong in that capability's own agent skill, not in this general Tabwright skill. If there is no exact match, use `capability search` and `capability describe` before running or creating new automation.
|
|
38
|
+
|
|
39
|
+
### Sandboxed agent environments
|
|
40
|
+
|
|
41
|
+
Tabwright controls the real user browser through a local relay on `127.0.0.1:19988`. If you are running inside an agent sandbox that restricts localhost access, use that environment's elevated/unsandboxed command mode for Tabwright commands that create sessions, list browsers, run `-s ... -e ...`, run browser-backed capabilities, or read relay logs. Do this immediately for Tabwright browser/relay commands instead of first trying a sandboxed command and diagnosing the relay as broken.
|
|
42
|
+
|
|
43
|
+
### Session management
|
|
44
|
+
|
|
45
|
+
If setup state is unclear, run `tabwright doctor` first. It checks the relay, Chrome extension, enabled tabs, active sessions, and visible capabilities, then prints one next action. Agents may use `tabwright doctor --json` to consume the same result without parsing terminal formatting. Existing sessions may belong to another task: create a new session unless the user explicitly handed you one.
|
|
46
|
+
|
|
47
|
+
Each session runs in an **isolated sandbox** with its own `state` object. Use sessions to:
|
|
48
|
+
|
|
49
|
+
- Keep state separate between different tasks or agents
|
|
50
|
+
- Persist data (pages, variables) across multiple execute calls
|
|
51
|
+
- Avoid interference when multiple agents use tabwright simultaneously
|
|
52
|
+
|
|
53
|
+
Get a new session ID to use in commands:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
tabwright session new
|
|
57
|
+
# outputs: 1
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`session new` automatically uses the only connected extension. When multiple extension profiles reconnect, it waits briefly for the set to settle and auto-selects only if exactly one profile has enabled tabs. Otherwise, use a key printed in the error or by `tabwright browser list`, then retry with `tabwright session new --browser <key>`. After replacing an older relay, Tabwright waits until the relay reports the current or a newer compatible package version before creating the session.
|
|
61
|
+
|
|
62
|
+
**Always use your own session** - pass `-s <id>` to all commands. Using the same session preserves your `state` between calls. Using a different session gives you a fresh `state`.
|
|
63
|
+
|
|
64
|
+
List all active sessions with their state keys:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
tabwright session list
|
|
68
|
+
# ID State Keys
|
|
69
|
+
# --------------
|
|
70
|
+
# 1 myPage, userData
|
|
71
|
+
# 2 -
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Reset a session if the browser connection is stale or broken:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
tabwright session reset <sessionId>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Remote access (control browser from another machine)
|
|
81
|
+
|
|
82
|
+
Tabwright can control a Chrome browser running on a different machine over the internet. The host machine runs `tabwright serve` with a [traforo](https://traforo.dev) tunnel, and the remote machine connects through the tunnel URL.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Host machine (has Chrome + extension)
|
|
86
|
+
npx -y traforo -p 19988 -- npx -y tabwright serve --token MY_SECRET_TOKEN
|
|
87
|
+
|
|
88
|
+
# Remote machine
|
|
89
|
+
export TABWRIGHT_HOST=https://<tunnel-id>-tunnel.traforo.dev
|
|
90
|
+
export TABWRIGHT_TOKEN=MY_SECRET_TOKEN
|
|
91
|
+
tabwright session new
|
|
92
|
+
tabwright -s 1 -e "await page.goto('https://example.com')"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
For the full guide (Docker, LAN, MCP config, security), see: https://playwriter.dev/docs/remote-access
|
|
96
|
+
|
|
97
|
+
### Direct CDP connection (no extension needed)
|
|
98
|
+
|
|
99
|
+
Tabwright can connect directly to a Chrome instance via the Chrome DevTools Protocol, bypassing the browser extension entirely. This is useful for:
|
|
100
|
+
|
|
101
|
+
- Chrome running with remote debugging enabled (CI, Docker, headless environments)
|
|
102
|
+
- Cloud browser providers that expose a CDP endpoint (e.g. `wss://xxx.cdp.browser-use.com`)
|
|
103
|
+
- Any service or machine that gives you a `ws://` or `wss://` URL to a Chrome DevTools session
|
|
104
|
+
|
|
105
|
+
**Prerequisites:** you need a CDP-enabled Chrome. Either:
|
|
106
|
+
|
|
107
|
+
- Open `chrome://inspect/#remote-debugging` in Chrome
|
|
108
|
+
- Launch Chrome with `--remote-debugging-port=9222`
|
|
109
|
+
- Use `tabwright browser start` (enables debugging automatically)
|
|
110
|
+
- Use a cloud browser provider URL (no local Chrome needed)
|
|
111
|
+
|
|
112
|
+
**CLI usage:**
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Auto-discover local Chrome instances with debugging enabled
|
|
116
|
+
tabwright session new --direct
|
|
117
|
+
|
|
118
|
+
# Connect to a specific CDP endpoint (local or cloud browser provider)
|
|
119
|
+
tabwright session new --direct ws://localhost:9222/devtools/browser/...
|
|
120
|
+
tabwright session new --direct wss://xxx.cdp.browser-use.com
|
|
121
|
+
|
|
122
|
+
# Connect to a remote Chrome instance (host:port auto-resolves to ws://)
|
|
123
|
+
tabwright session new --direct 192.168.1.50:9222
|
|
124
|
+
|
|
125
|
+
# Then use the session normally
|
|
126
|
+
tabwright -s 1 -e "await page.goto('https://example.com')"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**MCP configuration** (for AI assistants): set the `TABWRIGHT_DIRECT` env var in your MCP client config. If the user provides a CDP URL (like `wss://xxx.cdp.browser-use.com`), use it as the value:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"mcpServers": {
|
|
134
|
+
"tabwright": {
|
|
135
|
+
"command": "npx",
|
|
136
|
+
"args": ["-y", "tabwright@latest"],
|
|
137
|
+
"env": {
|
|
138
|
+
"TABWRIGHT_DIRECT": "wss://xxx.cdp.browser-use.com"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`TABWRIGHT_DIRECT` accepts:
|
|
146
|
+
|
|
147
|
+
- `1` — auto-discover Chrome on port 9222
|
|
148
|
+
- `ws://` or `wss://` URL — explicit WebSocket endpoint (local or cloud browser provider)
|
|
149
|
+
- `host:port` — resolves via HTTP probe to a ws:// URL
|
|
150
|
+
|
|
151
|
+
**Limitations:** DOM replay recording (`replay.start`/`replay.stop`) requires the Tabwright extension. Direct CDP mode can still execute browser automation, but it cannot collect extension-side rrweb replay files.
|
|
152
|
+
|
|
153
|
+
### Headless browser (no extension, no user browser)
|
|
154
|
+
|
|
155
|
+
Launch a headless Chrome automatically. No extension setup, no user browser involvement. Useful when the user doesn't want their personal browser used, in CI/server environments, or for fully autonomous automation.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# Install Chrome for Testing (first time only, if no Chrome is available)
|
|
159
|
+
tabwright browser install
|
|
160
|
+
|
|
161
|
+
# Launch headless Chrome and create a session
|
|
162
|
+
tabwright session new --browser headless
|
|
163
|
+
|
|
164
|
+
# Use the session normally
|
|
165
|
+
tabwright -s 1 -e "await page.goto('https://example.com')"
|
|
166
|
+
tabwright -s 1 -e "console.log(await snapshot({ page }))"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Multiple sessions reuse the same headless Chrome process. Extension-side replay recording is not available in headless mode.
|
|
170
|
+
|
|
171
|
+
If no Chrome binary is found, `tabwright session new --browser headless` will tell you to run `tabwright browser install` first to download Chrome for Testing.
|
|
172
|
+
|
|
173
|
+
### Cloud browsers (stealth, proxies, CAPTCHA solving)
|
|
174
|
+
|
|
175
|
+
Cloud browsers are full Chromium instances running in the cloud. They work exactly like a local Chrome session but with stealth and anti-detection built in. No local Chrome or extension needed.
|
|
176
|
+
|
|
177
|
+
**When to use cloud browsers:**
|
|
178
|
+
|
|
179
|
+
- **CAPTCHA bypass.** Cloudflare Turnstile, reCAPTCHA v2/v3, and hCaptcha are solved automatically via token injection. No API keys, no manual solving, no extra code.
|
|
180
|
+
- **Anti-detection.** Stealth Chromium patches remove `navigator.webdriver`, CDP leak fingerprints, and other automation signals. Sites that block Playwright, Puppeteer, or Selenium work normally.
|
|
181
|
+
- **Residential proxies.** Route traffic through residential IPs in 195+ countries with `--proxy <region>`. Proxy is disabled by default to save cost; enable it only when you need anti-detection or geo-targeting.
|
|
182
|
+
- **VPS and headless environments.** Run browser automation from any server without installing Chrome. The cloud browser runs remotely and you connect via CDP.
|
|
183
|
+
- **Parallel execution.** Spin up multiple cloud browsers to run tasks in parallel with subagents. Each browser is an isolated instance with its own IP, fingerprint, and cookie jar.
|
|
184
|
+
- **Multiple identities.** Control separate logged-in accounts on the same site simultaneously. Each cloud browser has independent cookies and storage, so sessions don't interfere with each other.
|
|
185
|
+
|
|
186
|
+
**Authentication:** two options depending on your environment.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Option 1: Interactive login (opens browser for OAuth)
|
|
190
|
+
tabwright cloud login
|
|
191
|
+
|
|
192
|
+
# Option 2: API key (for CI, VPS, headless — no browser needed)
|
|
193
|
+
# Create one at https://playwriter.dev/dashboard, then:
|
|
194
|
+
export TABWRIGHT_API_KEY=pw_xxxxx
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
# Check active cloud sessions
|
|
199
|
+
tabwright cloud status
|
|
200
|
+
|
|
201
|
+
# Start a cloud browser session (no proxy, cheapest)
|
|
202
|
+
tabwright session new --browser cloud
|
|
203
|
+
|
|
204
|
+
# Start with US residential proxy (for anti-detection / geo-targeting)
|
|
205
|
+
tabwright session new --browser cloud --proxy us
|
|
206
|
+
|
|
207
|
+
# Use a different region
|
|
208
|
+
tabwright session new --browser cloud --proxy de
|
|
209
|
+
|
|
210
|
+
# Use a custom proxy
|
|
211
|
+
tabwright session new --browser cloud --custom-proxy user:pass@host:8080
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Cloud sessions auto-stop after 10 minutes of inactivity. When proxy is enabled, raster images are blocked by default to reduce bandwidth costs. Pass `--disable-proxy-bandwidth-acceleration` if you need images to load.
|
|
215
|
+
|
|
216
|
+
### Execute code
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
tabwright -s <sessionId> -e "<code>"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The `-s` flag specifies a session ID (required). Get one with `tabwright session new`. Use the same session to persist state across commands.
|
|
223
|
+
|
|
224
|
+
**Examples:**
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# Navigate to a page
|
|
228
|
+
tabwright -s 1 -e 'state.page = await context.newPage(); await state.page.goto("https://example.com")'
|
|
229
|
+
|
|
230
|
+
# Click a button
|
|
231
|
+
tabwright -s 1 -e 'await state.page.click("button")'
|
|
232
|
+
|
|
233
|
+
# Get page title
|
|
234
|
+
tabwright -s 1 -e 'await state.page.title()'
|
|
235
|
+
|
|
236
|
+
# Take a screenshot
|
|
237
|
+
tabwright -s 1 -e 'await state.page.screenshot({ path: "/absolute/path/to/screenshot.png", scale: "css" })'
|
|
238
|
+
|
|
239
|
+
# Get accessibility snapshot
|
|
240
|
+
tabwright -s 1 -e 'await snapshot({ page: state.page })'
|
|
241
|
+
|
|
242
|
+
# Get accessibility snapshot for a specific iframe
|
|
243
|
+
tabwright -s 1 -e 'const frame = await state.page.locator("iframe").contentFrame(); await snapshot({ frame })'
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Why single quotes?** Always wrap `-e` code in single quotes (`'...'`) to prevent bash from interpreting `$`, backticks, and other special characters inside your JS code. Use double quotes or backtick template literals for strings inside the JS code.
|
|
247
|
+
|
|
248
|
+
**Multiline code:**
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
# Preferred: use heredoc with quoted delimiter (disables all bash expansion)
|
|
252
|
+
tabwright -s 1 -e "$(cat <<'EOF'
|
|
253
|
+
const links = await state.page.$$eval('a', els => els.map(e => e.href));
|
|
254
|
+
console.log('Found', links.length, 'links');
|
|
255
|
+
const price = text.match(/\$[\d.]+/);
|
|
256
|
+
EOF
|
|
257
|
+
)"
|
|
258
|
+
|
|
259
|
+
# Alternative: $'...' syntax (but beware: \n and \t become special, and
|
|
260
|
+
# single quotes inside must be escaped as \')
|
|
261
|
+
tabwright -s 1 -e $'
|
|
262
|
+
const title = await state.page.title();
|
|
263
|
+
const url = state.page.url();
|
|
264
|
+
console.log({ title, url });
|
|
265
|
+
'
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Quoting rules summary:**
|
|
269
|
+
- **Single quotes** (`'...'`): best for one-liners. No bash expansion at all. But you cannot include a literal single quote inside — use double quotes for JS strings instead.
|
|
270
|
+
- **Heredoc** (`<<'EOF'`): best for multiline code. The quoted `'EOF'` delimiter disables all bash expansion. Any character works inside, including `$`, backticks, and single quotes.
|
|
271
|
+
- **`$'...'`**: allows `\'` escaping but `\n`, `\t`, `\\` become special — conflicts with JS regex patterns.
|
|
272
|
+
|
|
273
|
+
### Execute from file
|
|
274
|
+
|
|
275
|
+
For longer scripts, use `-f` instead of `-e` to execute JavaScript from a file:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
tabwright -s 1 -f script.js
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The file is read from disk and executed in the same sandbox as `-e`. All context variables (`state`, `page`, `context`, etc.) are available. `-e` and `-f` cannot be used together.
|
|
282
|
+
|
|
283
|
+
### Saved capabilities
|
|
284
|
+
|
|
285
|
+
Saved capabilities are reusable Tabwright scripts with metadata, AI-readable intent, input schema, output schema, auth policy, trust status, and run logs. Use them to preserve repeated workflows such as querying a user in an admin console or calling a page-backed API without reopening Chrome.
|
|
286
|
+
|
|
287
|
+
Before writing new browser automation, search whether a saved capability already exists:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
tabwright capability list
|
|
291
|
+
tabwright capability route "current bilibili account"
|
|
292
|
+
tabwright capability search "current bilibili account"
|
|
293
|
+
tabwright capability describe bilibili-current-user --json
|
|
294
|
+
tabwright capability show query-user
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Create and edit capabilities. Use `--runtime node` for API/HTTP capabilities that can run without a browser. Use `--contract-file` to update the AI-readable contract (`whenToUse`, `whenNotToUse`, `sideEffect`, `auth`, schemas, examples, tags).
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
tabwright capability create query-user --project --title "Query user"
|
|
301
|
+
tabwright capability create bilibili-current-user --runtime node --title "Bilibili Current User"
|
|
302
|
+
tabwright capability update query-user --from-file script.js
|
|
303
|
+
tabwright capability update bilibili-current-user --contract-file contract.json
|
|
304
|
+
tabwright capability trust query-user
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Share a user-authored capability as a sanitized `.tgz`, or install it directly from a directory or HTTPS `.tgz` URL:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
tabwright capability pack query-user
|
|
311
|
+
tabwright capability install ./query-user.tgz
|
|
312
|
+
tabwright capability install ../shared-capabilities/query-user --project
|
|
313
|
+
tabwright capability install https://example.com/query-user.tgz
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Capability packages contain only `capability.json`, the configured entry script, optional `README.md`, and optional `agent-skills/`. The pack command excludes `secrets.json`, `runs.jsonl`, and `artifacts/`. A shared capability always installs as `draft`, even if its author trusted it locally. Inspect its contract and script, refresh auth with the recipient's own browser session when needed, validate it with `capability run --force`, and only then trust it. Packaged agent skills are not installed by default because they influence agent behavior; review one and run `capability skill install <id>`, or explicitly pass `--with-agent-skill`. Bundled suites such as `conan-config` remain installable by name and may be trusted by the publisher; use `--skip-agent-skills` for built-in suites.
|
|
317
|
+
|
|
318
|
+
When an AI is turning a user workflow into a durable capability, keep these responsibilities separate:
|
|
319
|
+
|
|
320
|
+
- Put machine-readable behavior in the capability contract: `match`, `routingHint`, schemas, `sideEffect`, `requiresConfirmation`, `auth`, and examples. For a capability with multiple safety boundaries, define `operations` keyed by `input.action`; each operation owns its match/routing metadata, schemas, permissions, side effect, and confirmation requirement.
|
|
321
|
+
- Put executable behavior in `script.js`.
|
|
322
|
+
- Put capability-specific agent instructions in an agent skill only when the capability is high-frequency, easy to misuse, has exact-match routing, needs auth/sandbox guidance, or needs nontrivial output/display rules.
|
|
323
|
+
- Do not rely on the CLI to write final skill prose. The CLI only scaffolds and installs; the AI that learned the workflow must edit the skill content.
|
|
324
|
+
|
|
325
|
+
Create an editable agent skill scaffold only when the capability needs one:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
tabwright capability skill init query-user
|
|
329
|
+
# edit .tabwright/capabilities/query-user/agent-skills/codex/SKILL.md
|
|
330
|
+
tabwright capability skill show query-user
|
|
331
|
+
tabwright capability skill install query-user
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
`capability skill install` refuses to install the untouched scaffold marker. Before installing, the AI should write: when to use the capability, when not to use it, the first command or route workflow, auth/sandbox notes, and the default output/display discipline. Simple capabilities usually do not need an agent skill; a strong contract is enough.
|
|
335
|
+
|
|
336
|
+
Run a capability with structured JSON input. `node` runtime capabilities run locally without opening Chrome. `browser` runtime capabilities create a headless session by default when `-s` is omitted; use `--browser user` when the capability needs the user's logged-in Chrome session.
|
|
337
|
+
|
|
338
|
+
If the selected operation has `requiresConfirmation: true`, stop and obtain explicit user approval for the concrete input and side effect. Only then rerun with its exact `confirmationToken`, typically `--confirm <capability-id>:<operation>`. Capabilities without operations continue to use `--confirm <capability-id>`. `--force` never bypasses this gate.
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
tabwright capability run query-user --input-json '{"email":"a@example.com"}' --json
|
|
342
|
+
tabwright capability run query-user -s 1 --input-json '{"email":"a@example.com"}'
|
|
343
|
+
tabwright capability run query-user --browser user --input-json '{"email":"a@example.com"}'
|
|
344
|
+
tabwright capability run update-user --browser user --input-json '{"email":"a@example.com"}' --confirm update-user
|
|
345
|
+
tabwright capability run bilibili-current-user --json
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
When multiple Chrome extension connections exist, pass a browser key from `tabwright browser list` instead of `user`.
|
|
349
|
+
|
|
350
|
+
When turning a user demonstration into a repeatable workflow, do not analyze during recording. Keep the recording/replay id as evidence, then generate a draft browser capability only after the user gives the id plus a concrete goal. Generated workflow scripts should run directly and return `needs_ai` with page context when the live page diverges.
|
|
351
|
+
|
|
352
|
+
Refresh cookie auth only after explicit user confirmation. This updates the local `secrets.json` and does not print cookie values:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
tabwright capability refresh-auth bilibili-current-user --browser user --json
|
|
356
|
+
tabwright capability refresh-auth bilibili-current-user --browser install:Chrome:qculboi03pt0 --json
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
The extension Options page shows the same local authentication state for cookie-authenticated capabilities. A user can review the declared cookie domains and explicitly authenticate or refresh with the current Chrome profile there. Treat that button click as the required user confirmation; never trigger the refresh endpoint automatically. Expiry is definitive when declared auth cookies have an expiry timestamp or a later run matches an auth failure signal. Session cookies without a validation failure remain authenticated with server-managed expiry.
|
|
360
|
+
|
|
361
|
+
Browser capability scripts run in the normal Tabwright sandbox and receive `input` and `capability` globals in addition to `page`, `context`, `state`, `snapshot`, and other helpers:
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
await page.goto("https://admin.example.com/users")
|
|
365
|
+
await page.getByPlaceholder("Search").fill(input.email)
|
|
366
|
+
await page.keyboard.press("Enter")
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
email: input.email,
|
|
370
|
+
url: page.url(),
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Node capability scripts receive `input`, `capability`, `secrets`, `artifacts`, `fetch`, URL helpers, timers, `Buffer`, text encoders, and `crypto` globals. Use `artifacts.writeJson({ filename, value })` and `artifacts.writeText({ filename, text })` to persist query results under the capability's scoped `artifacts` directory:
|
|
375
|
+
|
|
376
|
+
```js
|
|
377
|
+
const response = await fetch("https://api.example.com/me", {
|
|
378
|
+
headers: { cookie: secrets.cookieHeader },
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
const data = await response.json()
|
|
382
|
+
const filePath = artifacts.writeJson({ filename: "latest.json", value: data })
|
|
383
|
+
|
|
384
|
+
return { data, artifacts: { filePath } }
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Agents should use capability search and describe before creating new automation. A capability operation can be called autonomously only when the capability is `trusted`, that operation has `sideEffect: "read"`, and it has `requiresConfirmation: false`. Draft capabilities require `--force` before they can run. Confirmation-required operations additionally require their exact `confirmationToken` after explicit user approval; `--force` cannot substitute for approval. Editing a trusted capability's script automatically downgrades it to draft. Updating the AI contract through `--contract-file` also downgrades trusted capabilities to draft unless the patch explicitly sets a status. Use `tabwright studio` to start the standalone local management page for capabilities.
|
|
388
|
+
|
|
389
|
+
### Debugging tabwright issues
|
|
390
|
+
|
|
391
|
+
If some internal critical error happens you can read the relay server logs to understand the issue. The log file is located in the user home directory:
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
tabwright logfile # prints the log file path
|
|
395
|
+
# typically: ~/.tabwright/relay-server.log
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
The relay log contains logs from the extension, MCP and WS server. A separate CDP JSONL log is created alongside it (see `tabwright logfile`) with all CDP commands/responses and events, with long strings truncated. Both files are recreated every time the server starts. For debugging internal tabwright errors, read these files with grep/rg to find relevant lines.
|
|
399
|
+
|
|
400
|
+
Example: summarize CDP traffic counts by direction + method:
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
jq -r '.direction + "\t" + (.message.method // "response")' ~/.tabwright/cdp.jsonl | uniq -c
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
If you find a bug, you can create a gh issue using `gh issue create -R remorses/tabwright --title title --body body`. Ask for user confirmation before doing this.
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
# tabwright best practices
|
|
411
|
+
|
|
412
|
+
Control user's Chrome browser via playwright code snippets. Prefer single-line code with semicolons between statements. Use tabwright immediately without waiting for user actions; only if you get "extension is not connected" or "no browser tabs have Tabwright enabled" should you ask the user to click the tabwright extension icon on the target tab.
|
|
413
|
+
|
|
414
|
+
**When to use tabwright instead of webfetch/curl:** If a website is JS-heavy (SPAs like Instagram, Twitter, Facebook, etc.), has cookie consent modals, login walls, lazy-loaded content, carousels, or infinite scroll — **always use tabwright**. Simple fetch/webfetch will return an empty HTML shell with no content. Do NOT waste time trying curl, webfetch, or parsing raw HTML from JS-rendered sites. Go straight to tabwright: navigate with a real browser, dismiss modals, then extract what you need via `page.evaluate()` or network interception.
|
|
415
|
+
|
|
416
|
+
**If Chrome is not running**, the extension can't connect. Start Chrome from the command line before retrying:
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
# macOS
|
|
420
|
+
open -a "Google Chrome" --args --profile-directory=Default
|
|
421
|
+
|
|
422
|
+
# Linux
|
|
423
|
+
google-chrome --profile-directory=Default &
|
|
424
|
+
|
|
425
|
+
# Windows (cmd)
|
|
426
|
+
start chrome.exe --profile-directory=Default
|
|
427
|
+
|
|
428
|
+
# Windows (PowerShell)
|
|
429
|
+
Start-Process chrome.exe -ArgumentList '--profile-directory=Default'
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
You can collaborate with the user - they can help with captchas, difficult elements, or reproducing bugs.
|
|
433
|
+
|
|
434
|
+
**Direct CDP mode (no extension needed):** Tabwright can connect directly to Chrome's DevTools Protocol, bypassing the extension. This is useful in CI, Docker, headless environments, when Chrome has `--remote-debugging-port=9222`, or with cloud browser providers (e.g. `wss://xxx.cdp.browser-use.com`). If the user provides a CDP URL, set `TABWRIGHT_DIRECT` in the MCP client config:
|
|
435
|
+
|
|
436
|
+
```json
|
|
437
|
+
{
|
|
438
|
+
"mcpServers": {
|
|
439
|
+
"tabwright": {
|
|
440
|
+
"command": "npx",
|
|
441
|
+
"args": ["-y", "tabwright@latest"],
|
|
442
|
+
"env": {
|
|
443
|
+
"TABWRIGHT_DIRECT": "wss://xxx.cdp.browser-use.com"
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
`TABWRIGHT_DIRECT` accepts `1` (auto-discover Chrome on port 9222), a `ws://` or `wss://` endpoint (including cloud browser providers), or `host:port`. Extension-side replay recording is not available in direct CDP mode.
|
|
451
|
+
|
|
452
|
+
## context variables
|
|
453
|
+
|
|
454
|
+
- `state` - object persisted between calls **within your session**. Each session has its own isolated state. Use to store pages, data, listeners (e.g., `state.page = await context.newPage()`)
|
|
455
|
+
- `page` - a default page (may be shared with other agents). Prefer creating your own page and storing it in `state` (see "working with pages")
|
|
456
|
+
- `context` - browser context, access all pages via `context.pages()`
|
|
457
|
+
- `require` - load Node.js modules (e.g., `const fs = require('node:fs')`). ESM `import` is not available in the sandbox
|
|
458
|
+
- Node.js globals: `setTimeout`, `setInterval`, `fetch`, `URL`, `Buffer`, `crypto`, `process`, etc.
|
|
459
|
+
|
|
460
|
+
**Not available in the sandbox:** `__dirname`, `__filename`, `import`.
|
|
461
|
+
|
|
462
|
+
**Important:** `state` is **session-isolated** but pages are **shared** across all sessions. See "working with pages" for how to avoid interference.
|
|
463
|
+
|
|
464
|
+
**Sandboxed `fs` write restrictions:** `require('node:fs')` is scoped. Writes (writeFileSync, mkdirSync, etc.) only succeed in:
|
|
465
|
+
- The **directory where `tabwright` CLI was invoked** (the session's cwd)
|
|
466
|
+
- `/tmp`
|
|
467
|
+
- The OS temp directory (`os.tmpdir()`, e.g. `/var/folders/.../T/` on macOS)
|
|
468
|
+
|
|
469
|
+
Writing to any other path (e.g. `~/Downloads`, `~/Desktop`) throws `EPERM: operation not permitted, access outside allowed directories`. To save files elsewhere, write to a temp path first, then move the file using a shell command outside the sandbox.
|
|
470
|
+
|
|
471
|
+
## rules
|
|
472
|
+
|
|
473
|
+
- **Initialize state.page first**: see "working with pages" — at the start of a task, assign `state.page` (reuse `about:blank` or create one) and use `state.page` for all automation steps.
|
|
474
|
+
- **Multiple calls**: use multiple execute calls for complex logic - helps understand intermediate state and isolate which action failed
|
|
475
|
+
- **Never close**: never call `browser.close()` or `context.close()`. Only close pages you created or if user asks
|
|
476
|
+
- **No bringToFront**: never call unless user asks - it's disruptive and unnecessary, you can interact with background pages
|
|
477
|
+
- **Check state after actions**: always verify page state after clicking/submitting (see next section)
|
|
478
|
+
- **Clean up listeners**: call `state.page.removeAllListeners()` at end of message to prevent leaks
|
|
479
|
+
- **Always print page logs after every action**: call `getLatestLogs({ page: state.page, sinceLastCall: true })` after every goto, click, or submit to catch console errors and warnings. Do not manually collect `page.on('console')` events; manual listeners miss logs emitted before the listener is attached. The first `sinceLastCall` call returns all buffered logs including startup and hydration errors.
|
|
480
|
+
- **CDP sessions**: use `getCDPSession({ page: state.page })` not `state.page.context().newCDPSession()` - NEVER use `newCDPSession()` method, it doesn't work through tabwright relay
|
|
481
|
+
- **Wait for load**: use `state.page.waitForLoadState('domcontentloaded')` not `state.page.waitForEvent('load')` - waitForEvent times out if already loaded
|
|
482
|
+
- **Minimize timeouts**: prefer proper waits (`waitForSelector`, `waitForPageLoad`) over `state.page.waitForTimeout()`. Short timeouts (1-2s) are acceptable for non-deterministic events like animations, tab opens, or async UI updates where no specific selector is available
|
|
483
|
+
- **Snapshot before screenshot**: always use `snapshot()` first to understand page state (text-based, fast, cheap). Only use `screenshot` when you specifically need visual/spatial information. Never take a screenshot just to check if a page loaded or to read text content — snapshot gives you that instantly without burning image tokens
|
|
484
|
+
- **Always use absolute file paths for Playwright artifact APIs**: for `page.screenshot({ path })`, `locator.screenshot({ path })`, `elementHandle.screenshot({ path })`, `page.pdf({ path })`, `download.saveAs(path)`, and `video.saveAs(path)`, always pass an absolute path. Relative paths are resolved by Playwright client internals, not the sandboxed `fs`, so they may use the relay server cwd instead of your session cwd.
|
|
485
|
+
- **Snapshot replaces page.evaluate() for inspection**: do NOT write `page.evaluate()` calls to manually query class names, bounding boxes, child counts, or visibility flags. `snapshot()` already shows every interactive element with its text, role, and a ready-to-use locator. If you catch yourself writing `document.querySelector` or `getBoundingClientRect` inside evaluate — stop and use `snapshot()` instead. Reserve `page.evaluate()` for actions that modify page state (e.g., `localStorage.clear()`, scroll manipulation) or extract non-DOM data (e.g., `window.__CONFIG__`)
|
|
486
|
+
|
|
487
|
+
## interaction feedback loop
|
|
488
|
+
|
|
489
|
+
Every browser interaction must follow **observe → act → observe**. Never chain multiple actions blindly.
|
|
490
|
+
|
|
491
|
+
1. **Open page** — get or create your page, navigate to URL
|
|
492
|
+
2. **Observe** — print `state.page.url()` + `snapshot()` + `getLatestLogs({ sinceLastCall: true })`. Always print URL — pages can redirect unexpectedly.
|
|
493
|
+
3. **Check** — if page isn't ready (loading, wrong URL, content missing), wait and observe again
|
|
494
|
+
4. **Act** — perform one action (click, type, submit)
|
|
495
|
+
5. **Observe again** — print URL + snapshot + page logs to verify the action's effect
|
|
496
|
+
6. **Repeat** from step 3 until task is complete
|
|
497
|
+
|
|
498
|
+
**Always print page logs after every action** using `getLatestLogs({ sinceLastCall: true })`. This returns only new console messages and errors since the last call, so you catch hydration errors, failed network requests, and runtime exceptions without duplicates. The first call returns all buffered logs from the page, including logs emitted before your script started.
|
|
499
|
+
|
|
500
|
+
```js
|
|
501
|
+
// Each step should be a separate execute call:
|
|
502
|
+
// Step 1: navigate + observe
|
|
503
|
+
state.page = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage())
|
|
504
|
+
await state.page.goto('https://example.com', { waitUntil: 'domcontentloaded' })
|
|
505
|
+
console.log('URL:', state.page.url())
|
|
506
|
+
console.log('Page logs:', await getLatestLogs({ page: state.page, sinceLastCall: true }))
|
|
507
|
+
await snapshot({ page: state.page }).then(console.log)
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
```js
|
|
511
|
+
// Step 2: act + observe
|
|
512
|
+
await state.page.locator('button:has-text("Submit")').click()
|
|
513
|
+
console.log('URL:', state.page.url())
|
|
514
|
+
console.log('Page logs:', await getLatestLogs({ page: state.page, sinceLastCall: true }))
|
|
515
|
+
await snapshot({ page: state.page }).then(console.log)
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
If nothing changed after an action, try `waitForPageLoad({ page: state.page, timeout: 3000 })` or you may have clicked the wrong element.
|
|
519
|
+
|
|
520
|
+
**Deeper observation** — when snapshots aren't enough to understand what happened, combine snapshot with filtered logs:
|
|
521
|
+
|
|
522
|
+
```js
|
|
523
|
+
// Search for specific errors in all logs (not just since last call)
|
|
524
|
+
const errors = await getLatestLogs({ page: state.page, search: /error|fail/i, count: 20 })
|
|
525
|
+
|
|
526
|
+
// Combine snapshot + filtered logs for full picture
|
|
527
|
+
const snap = await snapshot({ page: state.page, search: /dialog|error|message/ })
|
|
528
|
+
const logs = await getLatestLogs({ page: state.page, search: /error/i, count: 10 })
|
|
529
|
+
console.log('UI:', snap)
|
|
530
|
+
console.log('Logs:', logs)
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Use `getLatestLogs({ sinceLastCall: true })` after every action, `getLatestLogs({ search })` for targeted debugging, `state.page.url()` for navigation, screenshots only for visual layout issues.
|
|
534
|
+
|
|
535
|
+
## common mistakes to avoid
|
|
536
|
+
|
|
537
|
+
**1. Not verifying actions succeeded**
|
|
538
|
+
Always check page state after important actions (form submissions, uploads, typing). Your mental model can diverge from actual browser state:
|
|
539
|
+
|
|
540
|
+
```js
|
|
541
|
+
await state.page.keyboard.type('my text')
|
|
542
|
+
await snapshot({ page: state.page, search: /my text/ })
|
|
543
|
+
// If verifying visual layout specifically, use screenshotWithAccessibilityLabels instead
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
**2. Assuming paste/upload worked**
|
|
547
|
+
Clipboard paste (`Meta+v`) can silently fail. For file uploads, prefer file input:
|
|
548
|
+
|
|
549
|
+
```js
|
|
550
|
+
// Reliable: use file input
|
|
551
|
+
const fileInput = state.page.locator('input[type="file"]').first()
|
|
552
|
+
await fileInput.setInputFiles('/path/to/image.png')
|
|
553
|
+
|
|
554
|
+
// Unreliable: clipboard paste may silently fail, need to focus textarea first for example
|
|
555
|
+
await state.page.keyboard.press('Meta+v') // always verify with screenshot!
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**3. Using stale locators from old snapshots**
|
|
559
|
+
Locators (especially ones with `>> nth=`) can change when the page updates. Always get a fresh snapshot before clicking, then immediately use locators from that output:
|
|
560
|
+
|
|
561
|
+
```js
|
|
562
|
+
await snapshot({ page: state.page, showDiffSinceLastCall: true })
|
|
563
|
+
// Now use the NEW locators from this output
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
**4. Wrong assumptions about current page/element**
|
|
567
|
+
Before destructive actions (delete, submit), verify you're targeting the right thing:
|
|
568
|
+
|
|
569
|
+
```js
|
|
570
|
+
// Before deleting, verify it's the right item
|
|
571
|
+
await screenshotWithAccessibilityLabels({ page: state.page })
|
|
572
|
+
// READ the screenshot to confirm, THEN proceed with delete
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**5. Text concatenation without line breaks**
|
|
576
|
+
`keyboard.type()` doesn't insert newlines from `\n` in strings. Use `keyboard.press('Enter')` between lines:
|
|
577
|
+
|
|
578
|
+
```js
|
|
579
|
+
await state.page.keyboard.type('Line 1')
|
|
580
|
+
await state.page.keyboard.press('Enter')
|
|
581
|
+
await state.page.keyboard.type('Line 2')
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
**6. Quote escaping in bash**
|
|
585
|
+
Bash parses `$`, backticks, and `\` inside double-quoted strings. This silently corrupts JS code. Always use single quotes or heredoc:
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
# single quotes — bash passes everything through literally
|
|
589
|
+
tabwright -s 1 -e 'await state.page.locator(`[id="_r_a_"]`).click()'
|
|
590
|
+
|
|
591
|
+
# heredoc for complex code with mixed quotes
|
|
592
|
+
tabwright -s 1 -e "$(cat <<'EOF'
|
|
593
|
+
await state.page.locator('[id="_r_a_"]').click()
|
|
594
|
+
const match = html.match(/\$[\d.]+/g)
|
|
595
|
+
EOF
|
|
596
|
+
)"
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
**7. Using screenshots when snapshots suffice**
|
|
600
|
+
Screenshots + image analysis is expensive and slow. Only use screenshots for visual/CSS issues. Use snapshot for text checks:
|
|
601
|
+
|
|
602
|
+
```js
|
|
603
|
+
await snapshot({ page: state.page, search: /expected text/i })
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**8. Assuming page content loaded**
|
|
607
|
+
Even after `goto()`, dynamic content may not be ready:
|
|
608
|
+
|
|
609
|
+
```js
|
|
610
|
+
await state.page.goto('https://example.com')
|
|
611
|
+
// Content may still be loading via JavaScript!
|
|
612
|
+
await state.page.waitForSelector('article', { timeout: 10000 })
|
|
613
|
+
// Or use waitForPageLoad utility
|
|
614
|
+
await waitForPageLoad({ page: state.page, timeout: 5000 })
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
**9. Not using tabwright for JS-rendered sites**
|
|
618
|
+
Do NOT waste context trying webfetch, curl, or Playwright CLI screenshots on SPAs (Instagram, Twitter, etc.). These return empty HTML shells. Use tabwright directly:
|
|
619
|
+
|
|
620
|
+
```js
|
|
621
|
+
state.page = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage())
|
|
622
|
+
await state.page.goto('https://www.instagram.com/p/ABC123/', { waitUntil: 'domcontentloaded' })
|
|
623
|
+
await waitForPageLoad({ page: state.page, timeout: 8000 })
|
|
624
|
+
await snapshot({ page: state.page, search: /cookie|consent|accept/i }).then(console.log)
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**10. Login buttons that open popups**
|
|
628
|
+
Popup windows (`window.open` with features, OAuth buttons) are auto-relocated to tabs in the main window by the Tabwright extension. The new tab appears in `context.pages()` and is fully controllable. You will receive a `[WARNING] New page opened from current page (index N, initial url: ...)` message pointing to the new tab — the `initial url` may be `about:blank` for blank-then-scripted popups, so check `context.pages()[N].url()` for the final URL:
|
|
629
|
+
|
|
630
|
+
```js
|
|
631
|
+
await state.page.locator('button:has-text("Login with Google")').click()
|
|
632
|
+
await state.page.waitForTimeout(1000)
|
|
633
|
+
|
|
634
|
+
// New tab is the last page in the context
|
|
635
|
+
const pages = context.pages()
|
|
636
|
+
const loginPage = pages[pages.length - 1]
|
|
637
|
+
|
|
638
|
+
// Complete login flow in loginPage, cookies are shared with original page
|
|
639
|
+
await loginPage.locator('[data-email]').first().click()
|
|
640
|
+
await loginPage.waitForURL('**/callback**')
|
|
641
|
+
// Original page should now be authenticated
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
**11. Click times out or does nothing — snapshot to find the blocker**
|
|
645
|
+
When a click times out, a **modal or overlay** is likely intercepting pointer events. Do not retry with different selectors or `{ force: true }` — snapshot to find the blocker:
|
|
646
|
+
|
|
647
|
+
```js
|
|
648
|
+
// click timed out → don't retry blindly, find what's blocking
|
|
649
|
+
await snapshot({ page: state.page, search: /dialog|modal/i })
|
|
650
|
+
// Found modal → interact with it properly (don't just close via X, it may reappear)
|
|
651
|
+
await state.page.getByRole('radio', { name: 'Nope, Vanilla' }).click()
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
**12. Never use `dispatchEvent` or `{ force: true }` to bypass blockers**
|
|
655
|
+
`dispatchEvent(new MouseEvent(...))`, `{ force: true }`, and `element.click()` inside `page.evaluate()` bypass Playwright checks but **do not trigger React/Vue/Svelte handlers** — state won't update. Use snapshot to find the real interactive element:
|
|
656
|
+
|
|
657
|
+
```js
|
|
658
|
+
await state.page.getByRole('radio', { name: 'Node.js' }).click()
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
**13. Over-investigating instead of just interacting**
|
|
662
|
+
When something doesn't respond to a click, do NOT start inspecting CDP event listeners, React fibers, canvas pixel data, or writing `page.evaluate()` to read class names and bounding boxes. This wastes massive context. Instead:
|
|
663
|
+
|
|
664
|
+
1. Take a `snapshot()` — it shows every interactive element and what to click
|
|
665
|
+
2. Try a different interaction pattern if `click()` didn't work:
|
|
666
|
+
- **Drawing/annotation tools, canvas paint** → `mouse.down`, move with steps, `mouse.up` (see drag section)
|
|
667
|
+
- **Keyboard-activated modes** → press the shortcut key (snapshot shows tooltip text like "Draw mode D")
|
|
668
|
+
- **Sliders, timeline scrubbers** → drag pattern
|
|
669
|
+
- **Collapsed/toggled toolbars** → click the toggle first, wait, then interact
|
|
670
|
+
3. Take another `snapshot()` to see what changed
|
|
671
|
+
4. Only investigate DOM internals if correct interaction patterns produce zero response after 2–3 attempts
|
|
672
|
+
|
|
673
|
+
## accessibility snapshots
|
|
674
|
+
|
|
675
|
+
```js
|
|
676
|
+
await snapshot({ page: state.page, search?, showDiffSinceLastCall? })
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
- `search` - string/regex to filter results (returns first 10 matching lines)
|
|
680
|
+
- `showDiffSinceLastCall` - returns diff since last snapshot (default: `true`, but `false` when `search` is provided). Pass `false` to get full snapshot.
|
|
681
|
+
|
|
682
|
+
Snapshots return full content on first call, then diffs on subsequent calls. Diff is only returned when shorter than full content. If nothing changed, returns "No changes since last snapshot" message. Use `showDiffSinceLastCall: false` to always get full content. When `search` is provided, diffing is disabled by default so the search filters the full content — pass `showDiffSinceLastCall: true` explicitly to combine both. This diffing behavior also applies to `getCleanHTML` and `getPageMarkdown`.
|
|
683
|
+
|
|
684
|
+
Example output:
|
|
685
|
+
|
|
686
|
+
```md
|
|
687
|
+
- banner:
|
|
688
|
+
- link "Home" [id="nav-home"]
|
|
689
|
+
- navigation:
|
|
690
|
+
- link "Docs" [data-testid="docs-link"]
|
|
691
|
+
- link "Blog" role=link[name="Blog"]
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Each interactive line ends with a Playwright locator you can pass to `state.page.locator()`.
|
|
695
|
+
If multiple elements share the same locator, a `>> nth=N` suffix is added (0-based)
|
|
696
|
+
to make it unique.
|
|
697
|
+
|
|
698
|
+
**Use snapshot locators directly — never invent selectors.** The snapshot output IS the selector. Do not guess CSS selectors or `getByText` when the snapshot already gives you the exact match:
|
|
699
|
+
|
|
700
|
+
```js
|
|
701
|
+
// Snapshot shows: role=radio[name="Nope, Vanilla"] → use it directly
|
|
702
|
+
await state.page.getByRole('radio', { name: 'Nope, Vanilla' }).click()
|
|
703
|
+
// Snapshot shows: role=link[name="SIGN IN"] → or pass raw string to locator()
|
|
704
|
+
await state.page.locator('role=link[name="SIGN IN"]').click()
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
**Beware CSS text-transform**: snapshots show visual text (`heading "NODE.JS"`) but DOM may be `"Node.js"`. Use case-insensitive regex: `getByRole('heading', { name: /node\.js/i })`.
|
|
708
|
+
|
|
709
|
+
If a screenshot shows ref labels like `e3`, resolve them using the last snapshot:
|
|
710
|
+
|
|
711
|
+
```js
|
|
712
|
+
const snap = await snapshot({ page: state.page })
|
|
713
|
+
const locator = refToLocator({ ref: 'e3' })
|
|
714
|
+
await state.page.locator(locator!).click()
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
Search for specific elements:
|
|
718
|
+
|
|
719
|
+
```js
|
|
720
|
+
const snap = await snapshot({ page: state.page, search: /button|submit/i })
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
**Scoping snapshots to a specific element** — pass a `locator` instead of `page` to snapshot only a subtree. This dramatically reduces output size when you only care about one section of the page (e.g., the main content area, ignoring the sidebar/header/footer):
|
|
724
|
+
|
|
725
|
+
```js
|
|
726
|
+
// Full page snapshot: ~150 lines (sidebar, nav, header, footer, everything)
|
|
727
|
+
await snapshot({ page: state.page })
|
|
728
|
+
|
|
729
|
+
// Scoped to main: ~20 lines (just the content you care about)
|
|
730
|
+
await snapshot({ locator: state.page.locator('main') })
|
|
731
|
+
|
|
732
|
+
// Scope to a specific form, dialog, or section
|
|
733
|
+
await snapshot({ locator: state.page.locator('[role="dialog"]') })
|
|
734
|
+
await snapshot({ locator: state.page.locator('form#checkout') })
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
Use this whenever the full page snapshot is dominated by navigation or layout elements you don't need. It saves significant tokens and makes the output much easier to parse.
|
|
738
|
+
|
|
739
|
+
**Filtering large snapshots in JS** — when `search` isn't enough, filter the string directly: `snap.split('\n').filter(l => l.includes('dialog') || l.includes('error')).join('\n')`
|
|
740
|
+
|
|
741
|
+
## choosing between snapshot methods
|
|
742
|
+
|
|
743
|
+
Use `snapshot` for text-heavy pages (forms, articles) — fast, cheap, searchable. Use `screenshotWithAccessibilityLabels` for complex visual layouts (grids, galleries, dashboards) where spatial position matters. Both share the same ref system and can be combined.
|
|
744
|
+
|
|
745
|
+
## selector best practices
|
|
746
|
+
|
|
747
|
+
**For unknown websites**: use `snapshot()` - it shows what's actually interactive with stable locators.
|
|
748
|
+
|
|
749
|
+
**For development** (when you have source code access), prefer stable selectors in this order:
|
|
750
|
+
|
|
751
|
+
1. **Best**: `[data-testid="submit"]` - explicit test attributes, never change accidentally
|
|
752
|
+
2. **Good**: `getByRole('button', { name: 'Save' })` - accessible, semantic
|
|
753
|
+
3. **Good**: `getByText('Sign in')`, `getByLabel('Email')` - readable, user-facing
|
|
754
|
+
4. **OK**: `input[name="email"]`, `button[type="submit"]` - semantic HTML
|
|
755
|
+
5. **Avoid**: `.btn-primary`, `#submit` - classes/IDs change frequently
|
|
756
|
+
6. **Last resort**: `div.container > form > button` - fragile, breaks easily
|
|
757
|
+
|
|
758
|
+
Combine locators for precision:
|
|
759
|
+
|
|
760
|
+
```js
|
|
761
|
+
state.page.locator('tr').filter({ hasText: 'John' }).locator('button').click()
|
|
762
|
+
state.page.locator('button').nth(2).click()
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
If a locator matches multiple elements, Playwright throws "strict mode violation". Use `.first()`, `.last()`, or `.nth(n)`:
|
|
766
|
+
|
|
767
|
+
```js
|
|
768
|
+
await state.page.locator('button').first().click() // first match
|
|
769
|
+
await state.page.locator('.item').last().click() // last match
|
|
770
|
+
await state.page.locator('li').nth(3).click() // 4th item (0-indexed)
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
## working with pages
|
|
774
|
+
|
|
775
|
+
**Pages are shared, state is not.** `context.pages()` returns all browser tabs with tabwright enabled — shared across all sessions. Multiple agents see the same tabs. If another agent navigates or closes a page you're using, you'll be affected. To avoid interference, **get your own page**.
|
|
776
|
+
|
|
777
|
+
**Get or create your page (first call):**
|
|
778
|
+
|
|
779
|
+
On your very first execute call, reuse an existing empty tab or create a new one, and navigate it **in the same execute call**. Store it in `state` and use `state.page` for all subsequent operations instead of the default `page` variable:
|
|
780
|
+
|
|
781
|
+
```js
|
|
782
|
+
// Reuse an empty about:blank tab if available, otherwise create a new one.
|
|
783
|
+
// IMPORTANT: always navigate immediately in the same call to avoid another
|
|
784
|
+
// agent grabbing the same about:blank tab between execute calls.
|
|
785
|
+
state.page = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage())
|
|
786
|
+
await state.page.goto('https://example.com')
|
|
787
|
+
// Use state.page for ALL subsequent operations
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
**Handle page closures gracefully:**
|
|
791
|
+
|
|
792
|
+
The user may close your page by accident (e.g., closing a tab in Chrome). Always check before using it and recreate if needed:
|
|
793
|
+
|
|
794
|
+
```js
|
|
795
|
+
if (!state.page || state.page.isClosed()) {
|
|
796
|
+
state.page = context.pages().find((p) => p.url() === 'about:blank') ?? (await context.newPage())
|
|
797
|
+
}
|
|
798
|
+
await state.page.goto('https://example.com')
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
**Use an existing page only when the user asks:**
|
|
802
|
+
|
|
803
|
+
Only use a page from `context.pages()` if the user explicitly asks you to control a specific tab they already opened (e.g., they're logged into an app). Find it by URL pattern and store it in state:
|
|
804
|
+
|
|
805
|
+
```js
|
|
806
|
+
const pages = context.pages().filter((x) => x.url().includes('myapp.com'))
|
|
807
|
+
if (pages.length === 0) throw new Error('No myapp.com page found. Ask user to enable tabwright on it.')
|
|
808
|
+
if (pages.length > 1) throw new Error(`Found ${pages.length} matching pages, expected 1`)
|
|
809
|
+
state.targetPage = pages[0]
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
**List all available pages:**
|
|
813
|
+
|
|
814
|
+
```js
|
|
815
|
+
context.pages().map((p) => p.url())
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
**Popup windows become tabs automatically:**
|
|
819
|
+
|
|
820
|
+
The extension intercepts Chrome popup windows (`window.open(url, '', 'width=...')`, OAuth login flows) and relocates them into the main window as regular tabs. You don't need cmd+click or `{ modifiers: ['Meta'] }` to avoid popups. When a page opens another, you receive a `[WARNING] New page opened from current page (index N, initial url: ...)` and can access it via `context.pages()[N]`.
|
|
821
|
+
|
|
822
|
+
## navigation
|
|
823
|
+
|
|
824
|
+
**Use `domcontentloaded`** for `page.goto()`:
|
|
825
|
+
|
|
826
|
+
```js
|
|
827
|
+
await state.page.goto('https://example.com', { waitUntil: 'domcontentloaded' })
|
|
828
|
+
await waitForPageLoad({ page: state.page, timeout: 5000 })
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
## common patterns
|
|
832
|
+
|
|
833
|
+
**Authenticated fetches** - fetch from within page context to include session cookies automatically:
|
|
834
|
+
|
|
835
|
+
```js
|
|
836
|
+
const data = await state.page.evaluate(async (url) => {
|
|
837
|
+
const resp = await fetch(url)
|
|
838
|
+
return await resp.text()
|
|
839
|
+
}, 'https://example.com/protected/resource')
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
**Read page cookies via CDP** - use `Network.getCookies` on the page CDP session:
|
|
843
|
+
|
|
844
|
+
```js
|
|
845
|
+
const cdp = await getCDPSession({ page: state.page })
|
|
846
|
+
const { cookies } = await cdp.send('Network.getCookies', { urls: [state.page.url()] })
|
|
847
|
+
console.log(cookies)
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
MUST use this for page-scoped cookies in extension mode. `Storage.getCookies` is a root-session command and will fail in tabwright.
|
|
851
|
+
|
|
852
|
+
**NEVER use `Network.clearBrowserCookies` or `Network.clearBrowserCache`** — these CDP commands are **profile-wide destructive operations** that wipe ALL cookies/cache across every domain in the user's Chrome profile. They will log the user out of Gmail, GitHub, and every authenticated session.
|
|
853
|
+
|
|
854
|
+
**Clear cookies for a specific domain** — use `Network.getCookies` to fetch cookies scoped to URLs, then delete them individually with `Network.deleteCookies`:
|
|
855
|
+
|
|
856
|
+
```js
|
|
857
|
+
const cdp = await getCDPSession({ page: state.page })
|
|
858
|
+
const { cookies } = await cdp.send('Network.getCookies', {
|
|
859
|
+
urls: ['https://example.com', 'https://www.example.com'],
|
|
860
|
+
})
|
|
861
|
+
for (const cookie of cookies) {
|
|
862
|
+
await cdp.send('Network.deleteCookies', { name: cookie.name, domain: cookie.domain })
|
|
863
|
+
}
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
**Downloading large data** - console output truncates large strings. Trigger a browser download instead:
|
|
867
|
+
|
|
868
|
+
```js
|
|
869
|
+
// Fetch protected data and trigger download to user's Downloads folder
|
|
870
|
+
await state.page.evaluate(async (url) => {
|
|
871
|
+
const resp = await fetch(url)
|
|
872
|
+
const data = await resp.text()
|
|
873
|
+
const blob = new Blob([data], { type: 'application/octet-stream' })
|
|
874
|
+
const a = document.createElement('a')
|
|
875
|
+
a.href = URL.createObjectURL(blob)
|
|
876
|
+
a.download = 'data.json'
|
|
877
|
+
a.click()
|
|
878
|
+
}, 'https://example.com/protected/large-file')
|
|
879
|
+
// File saves to ~/Downloads - read it from there
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
**Avoid permission-gated browser APIs** - some APIs require user permission prompts or special browser flags. These often fail silently or hang. Examples to avoid:
|
|
883
|
+
|
|
884
|
+
- `navigator.clipboard.writeText()` - requires permission
|
|
885
|
+
- Multiple concurrent downloads - browser may block
|
|
886
|
+
- `window.showSaveFilePicker()` - requires user gesture
|
|
887
|
+
- Geolocation, camera, microphone APIs
|
|
888
|
+
|
|
889
|
+
Instead, use simpler alternatives (single download via `a.click()`, store data in `state`, etc).
|
|
890
|
+
|
|
891
|
+
**Downloads** - capture and save:
|
|
892
|
+
|
|
893
|
+
```js
|
|
894
|
+
const [download] = await Promise.all([state.page.waitForEvent('download'), state.page.click('button.download')])
|
|
895
|
+
await download.saveAs(`/absolute/path/${download.suggestedFilename()}`)
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
**iFrames** - two approaches depending on what you need:
|
|
899
|
+
|
|
900
|
+
```js
|
|
901
|
+
// frameLocator: for chaining locator operations (click, fill, etc.)
|
|
902
|
+
const frame = state.page.frameLocator('#my-iframe')
|
|
903
|
+
await frame.locator('button').click()
|
|
904
|
+
|
|
905
|
+
// contentFrame: returns a Frame object, needed for snapshot({ frame })
|
|
906
|
+
const frame2 = await state.page.locator('iframe').contentFrame()
|
|
907
|
+
await snapshot({ frame: frame2 })
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
**Dialogs** - handle alerts/confirms/prompts:
|
|
911
|
+
|
|
912
|
+
```js
|
|
913
|
+
state.page.on('dialog', async (dialog) => {
|
|
914
|
+
console.log(dialog.message())
|
|
915
|
+
await dialog.accept()
|
|
916
|
+
})
|
|
917
|
+
await state.page.click('button.trigger-alert')
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
**Handling page obstacles (cookie modals, login walls, age gates)** - most major websites show blocking overlays. Always check for these with `snapshot()` right after navigation and dismiss them before doing anything else:
|
|
921
|
+
|
|
922
|
+
```js
|
|
923
|
+
// After navigating, check for common obstacles
|
|
924
|
+
await waitForPageLoad({ page: state.page, timeout: 5000 })
|
|
925
|
+
const snap = await snapshot({
|
|
926
|
+
page: state.page,
|
|
927
|
+
search: /cookie|consent|accept|reject|decline|allow|age|verify|login|sign.in/i,
|
|
928
|
+
})
|
|
929
|
+
console.log(snap)
|
|
930
|
+
// Look for dismiss/accept/decline buttons in the snapshot, then click them:
|
|
931
|
+
// await state.page.locator('button:has-text("Accept")').click();
|
|
932
|
+
// await state.page.locator('button:has-text("Decline optional")').click();
|
|
933
|
+
// Then re-snapshot to confirm the modal is gone before proceeding
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
If the page requires login and the user is already logged into Chrome, their session cookies are available — just navigate and the page should load authenticated. If not, ask the user for help or use their existing logged-in tab via `context.pages()`.
|
|
937
|
+
|
|
938
|
+
**Extracting and downloading media (images, videos)** - use `page.evaluate()` to extract URLs from the rendered DOM, then download via Node.js in the sandbox. This is far more reliable than parsing raw HTML:
|
|
939
|
+
|
|
940
|
+
```js
|
|
941
|
+
// Extract all image URLs from rendered DOM
|
|
942
|
+
const images = await state.page.evaluate(() =>
|
|
943
|
+
Array.from(document.querySelectorAll('img[src]')).map((img) => ({
|
|
944
|
+
src: img.src,
|
|
945
|
+
alt: img.alt,
|
|
946
|
+
width: img.naturalWidth,
|
|
947
|
+
})),
|
|
948
|
+
)
|
|
949
|
+
console.log(JSON.stringify(images, null, 2))
|
|
950
|
+
|
|
951
|
+
// Download a specific image to disk
|
|
952
|
+
const fs = require('node:fs')
|
|
953
|
+
const resp = await fetch(images[0].src)
|
|
954
|
+
const buf = Buffer.from(await resp.arrayBuffer())
|
|
955
|
+
fs.writeFileSync('./downloaded-image.jpg', buf)
|
|
956
|
+
console.log('Saved', buf.length, 'bytes')
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
For carousels or lazy-loaded galleries, you may need to click navigation arrows or scroll first, then re-extract. Use network interception (see "network interception" section) to capture high-resolution CDN URLs that may differ from the `img.src` thumbnails.
|
|
960
|
+
|
|
961
|
+
## utility functions
|
|
962
|
+
|
|
963
|
+
**getLatestLogs** - retrieve captured browser console logs and page errors (up to 5000 per page):
|
|
964
|
+
|
|
965
|
+
Always use this helper when inspecting browser logs. Do not attach new `page.on('console')` listeners for debugging because they only see future events and can miss logs emitted during page startup or hydration.
|
|
966
|
+
|
|
967
|
+
Use `sinceLastCall: true` after every action to get only new logs since the previous call. The first call returns all buffered logs including pre-existing ones. Logs persist across navigations so you never miss errors from page transitions.
|
|
968
|
+
|
|
969
|
+
```js
|
|
970
|
+
await getLatestLogs({ page?, count?, search?, sinceLastCall? })
|
|
971
|
+
// After every action: get only new logs
|
|
972
|
+
const newLogs = await getLatestLogs({ page: state.page, sinceLastCall: true })
|
|
973
|
+
// Search all logs (ignores cursor):
|
|
974
|
+
const errors = await getLatestLogs({ search: /error/i, count: 50 })
|
|
975
|
+
const pageLogs = await getLatestLogs({ page: state.page, count: 100 })
|
|
976
|
+
const hydrationErrors = await getLatestLogs({ page: state.page, search: /hydration|pageerror|React/i })
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
**getCleanHTML** - get cleaned HTML from a locator or page, with search and diffing:
|
|
980
|
+
|
|
981
|
+
```js
|
|
982
|
+
await getCleanHTML({ locator, search?, showDiffSinceLastCall?, includeStyles? })
|
|
983
|
+
// Examples:
|
|
984
|
+
const html = await getCleanHTML({ locator: state.page.locator('body') })
|
|
985
|
+
const html = await getCleanHTML({ locator: state.page, search: /button/i })
|
|
986
|
+
const fullHtml = await getCleanHTML({ locator: state.page, showDiffSinceLastCall: false }) // disable diff
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
**Parameters:**
|
|
990
|
+
|
|
991
|
+
- `locator` - Playwright Locator or Page to get HTML from
|
|
992
|
+
- `search` - string/regex to filter results (returns first 10 matching lines with 5 lines context)
|
|
993
|
+
- `showDiffSinceLastCall` - returns diff since last call (default: `true`, but `false` when `search` is provided). Pass `false` to get full HTML.
|
|
994
|
+
- `includeStyles` - keep style and class attributes (default: false)
|
|
995
|
+
|
|
996
|
+
Cleans HTML automatically: removes script/style/svg/head tags, unwraps empty wrappers, removes empty elements, truncates long values. Keeps semantic attributes (`href`, `name`, `type`, `aria-*`, `data-*`).
|
|
997
|
+
|
|
998
|
+
**getPageMarkdown** - extract main page content as plain text using Mozilla Readability (same algorithm as Firefox Reader View). Strips navigation, ads, sidebars, and other clutter. Returns formatted text with title, author, and content:
|
|
999
|
+
|
|
1000
|
+
```js
|
|
1001
|
+
await getPageMarkdown({ page: state.page, search?, showDiffSinceLastCall? })
|
|
1002
|
+
// Examples:
|
|
1003
|
+
const content = await getPageMarkdown({ page: state.page, showDiffSinceLastCall: false }) // full article
|
|
1004
|
+
const matches = await getPageMarkdown({ page: state.page, search: /API/i }) // search within content
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
**Output format:**
|
|
1008
|
+
|
|
1009
|
+
```
|
|
1010
|
+
# Article Title
|
|
1011
|
+
|
|
1012
|
+
Author: John Doe | Site: example.com | Published: 2024-01-15
|
|
1013
|
+
|
|
1014
|
+
> Article excerpt or description
|
|
1015
|
+
|
|
1016
|
+
The main article content as plain text, with paragraphs preserved...
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
**Parameters:**
|
|
1020
|
+
|
|
1021
|
+
- `page` - Playwright Page to extract content from
|
|
1022
|
+
- `search` - string/regex to filter content (returns first 10 matching lines with 5 lines context)
|
|
1023
|
+
- `showDiffSinceLastCall` - returns diff since last call (default: `true`, but `false` when `search` is provided). Pass `false` to get full content.
|
|
1024
|
+
|
|
1025
|
+
**waitForPageLoad** - smart load detection that ignores analytics/ads:
|
|
1026
|
+
|
|
1027
|
+
```js
|
|
1028
|
+
await waitForPageLoad({ page: state.page, timeout?, pollInterval?, minWait? })
|
|
1029
|
+
// Returns: { success, readyState, pendingRequests, waitTimeMs, timedOut }
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
**getCDPSession** - send raw CDP commands:
|
|
1033
|
+
|
|
1034
|
+
```js
|
|
1035
|
+
const cdp = await getCDPSession({ page: state.page })
|
|
1036
|
+
const metrics = await cdp.send('Page.getLayoutMetrics')
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
**getLocatorStringForElement** - get stable Playwright selector from an element:
|
|
1040
|
+
|
|
1041
|
+
```js
|
|
1042
|
+
const selector = await getLocatorStringForElement(state.page.locator('[id="submit-btn"]'))
|
|
1043
|
+
// => "getByRole('button', { name: 'Save' })"
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
**getReactSource** - get React component source location (dev mode only):
|
|
1047
|
+
|
|
1048
|
+
```js
|
|
1049
|
+
const source = await getReactSource({ locator: state.page.locator('[data-testid="submit-btn"]') })
|
|
1050
|
+
// => { fileName, lineNumber, columnNumber, componentName }
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
**getReactComponentInfo** - get best-effort React component info for an element. Returns `null` for non-React elements and never throws just because an element was not rendered by React. Source locations are usually only available in React dev builds. Props are sanitized and truncated so functions, DOM nodes, circular refs, and huge objects do not flood the output.
|
|
1054
|
+
|
|
1055
|
+
```js
|
|
1056
|
+
const info = await getReactComponentInfo({ locator: state.page.locator('[data-testid="submit-btn"]') })
|
|
1057
|
+
// => { componentName, source, hierarchy, props } | null
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
**inspectPinnedElement** - inspect a Tabwright pinned element and print the element `outerHTML` plus React component info when available. Used by the in-page toolbar and right-click copy flow.
|
|
1061
|
+
|
|
1062
|
+
```js
|
|
1063
|
+
await inspectPinnedElement('https://example.com', 'globalThis.tabwrightPinnedElem1')
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
**getStylesForLocator** - inspect CSS styles applied to an element, like browser DevTools "Styles" panel. Useful for debugging styling issues, finding where a CSS property is defined (file:line), and checking inherited styles. Returns selector, source location, and declarations for each matching rule. ALWAYS fetch `https://playwriter.dev/resources/styles-api.md` first with curl or webfetch tool.
|
|
1067
|
+
|
|
1068
|
+
```js
|
|
1069
|
+
const styles = await getStylesForLocator({
|
|
1070
|
+
locator: state.page.locator('.btn'),
|
|
1071
|
+
cdp: await getCDPSession({ page: state.page }),
|
|
1072
|
+
})
|
|
1073
|
+
console.log(formatStylesAsText(styles))
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
**createDebugger** - set breakpoints, step through code, inspect variables at runtime. Useful for debugging issues that only reproduce in browser, understanding code flow, and inspecting state at specific points. Can pause on exceptions, evaluate expressions in scope, and blackbox framework code. ALWAYS fetch `https://playwriter.dev/resources/debugger-api.md` first.
|
|
1077
|
+
|
|
1078
|
+
```js
|
|
1079
|
+
const cdp = await getCDPSession({ page: state.page })
|
|
1080
|
+
const dbg = createDebugger({ cdp })
|
|
1081
|
+
await dbg.enable()
|
|
1082
|
+
const scripts = await dbg.listScripts({ search: 'app' })
|
|
1083
|
+
await dbg.setBreakpoint({ file: scripts[0].url, line: 42 })
|
|
1084
|
+
// when paused: dbg.inspectLocalVariables(), dbg.stepOver(), dbg.resume()
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
**createEditor** - view and live-edit page scripts and CSS at runtime. Edits are in-memory (persist until reload). Useful for testing quick fixes, searching page scripts with grep, and toggling debug flags. ALWAYS read `https://playwriter.dev/resources/editor-api.md` first.
|
|
1088
|
+
|
|
1089
|
+
```js
|
|
1090
|
+
const cdp = await getCDPSession({ page: state.page })
|
|
1091
|
+
const editor = createEditor({ cdp })
|
|
1092
|
+
await editor.enable()
|
|
1093
|
+
const matches = await editor.grep({ regex: /console\.log/ })
|
|
1094
|
+
await editor.edit({ url: matches[0].url, oldString: 'DEBUG = false', newString: 'DEBUG = true' })
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
**screenshotWithAccessibilityLabels** - take a screenshot with Vimium-style visual labels overlaid on interactive elements. Shows labels, captures screenshot, then removes labels. The image and accessibility snapshot are automatically included in the response. Can be called multiple times to capture multiple screenshots. Use a timeout of **20 seconds** for complex pages.
|
|
1098
|
+
|
|
1099
|
+
This is only for **finding interactive elements** on the page. To share a screenshot with the user or save an image, use `page.screenshot()` + `resizeImageForAgent()` instead (see "taking screenshots" section below).
|
|
1100
|
+
|
|
1101
|
+
Prefer this for pages with grids, image galleries, maps, or complex visual layouts where spatial position matters. For simple text-heavy pages, `snapshot` with search is faster and uses fewer tokens.
|
|
1102
|
+
|
|
1103
|
+
```js
|
|
1104
|
+
await screenshotWithAccessibilityLabels({ page: state.page })
|
|
1105
|
+
// Image and accessibility snapshot are automatically included in response
|
|
1106
|
+
// Use refs from snapshot to interact with elements
|
|
1107
|
+
await state.page.locator('[id="submit-btn"]').click()
|
|
1108
|
+
|
|
1109
|
+
// Can take multiple screenshots in one execution
|
|
1110
|
+
await screenshotWithAccessibilityLabels({ page: state.page })
|
|
1111
|
+
await state.page.click('button')
|
|
1112
|
+
await screenshotWithAccessibilityLabels({ page: state.page })
|
|
1113
|
+
// Both images are included in the response
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
Labels are color-coded: yellow=links, orange=buttons, coral=inputs, pink=checkboxes, peach=sliders, salmon=menus, amber=tabs.
|
|
1117
|
+
|
|
1118
|
+
**resizeImageForAgent** - shrink an image so it consumes fewer tokens when read back into context. The resized image is automatically included in the response (visible to the LLM). `await resizeImageForAgent({ input: '/absolute/path/to/screenshot.png' })`. Also accepts `width`, `height`, `maxDimension`, `quality`, `format` (default: `'png'`), `output`. Alias: `resizeImage`.
|
|
1119
|
+
|
|
1120
|
+
**replay.start / replay.stop** - record the page as an rrweb DOM replay. This captures DOM snapshots, mutations, inputs, mouse movement, scrolls, and user-added Tabwright annotations into `~/.tabwright/rrweb-recordings/<id>.json`, then plays back in the Tabwright extension options page. DOM replays are for review and workflow understanding only: clicking inside the replay does **not** execute the original page's React/Vue/business logic.
|
|
1121
|
+
|
|
1122
|
+
Use replay recordings when you need a compact, inspectable artifact for AI understanding, workflow compilation, and user review. The in-page toolbar records rrweb replay only; video capture is intentionally not part of the product.
|
|
1123
|
+
|
|
1124
|
+
While recording, the toolbar's element selection button becomes an annotation tool. If the user selects an element and writes a note, the note is saved as a `tabwright.annotation` rrweb custom event and appears in `replay index` output as `annotations`. Treat these annotations as stronger intent signals than inferred labels/selectors.
|
|
1125
|
+
|
|
1126
|
+
```js
|
|
1127
|
+
await replay.start({
|
|
1128
|
+
page: state.page,
|
|
1129
|
+
checkoutEveryNms: 0,
|
|
1130
|
+
maskAllInputs: false,
|
|
1131
|
+
recordCanvas: false,
|
|
1132
|
+
inlineImages: false,
|
|
1133
|
+
})
|
|
1134
|
+
|
|
1135
|
+
await state.page.getByLabel('Title').fill('Summer banner')
|
|
1136
|
+
await state.page.getByRole('button', { name: 'Preview' }).click()
|
|
1137
|
+
|
|
1138
|
+
state.replayResult = await replay.stop({ page: state.page })
|
|
1139
|
+
console.log(state.replayResult)
|
|
1140
|
+
|
|
1141
|
+
// Other: replay.isRecording({ page }), replay.cancel({ page }), replay.list({ limit: 10 }),
|
|
1142
|
+
// replay.events({ id: state.replayResult.id })
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
**workflow.saveFromRecording / workflow.saveCapability** - after the user gives a demonstration replay id and a concrete goal, save the derived reusable flow as a project capability. Prefer `workflow.saveFromRecording()` when the flow can be represented as structured steps; use `workflow.saveCapability()` when you need to write a custom script. Saved workflows start as `draft`, have `sideEffect: "write"` and `requiresConfirmation: true` by default, and can later be inspected or run with `tabwright capability ...`. The generated script runs the live frontend flow, observes the expected final request when `finalRequest` is provided, and returns `needs_ai` with a snapshot when the page no longer matches the replay. Omit `finalRequest` for flows that do not have a real submit/request boundary.
|
|
1146
|
+
|
|
1147
|
+
**replay list** - use `tabwright replay list --limit 10 --json` to discover saved demonstrations without connecting to the relay. Results are newest-first and include the exact inspect and make commands for each replay.
|
|
1148
|
+
|
|
1149
|
+
**replay make / replay compile** - when the user gives an rrweb replay id and asks to run similar work, first compile the replay into a project capability instead of manually replaying every step. Prefer `tabwright replay make <replayId> <capabilityId> --force --goal "..." --json` because it builds the AI index and compiles in one step. A successful result has `status: "compiled"` and writes the draft capability. If the deterministic compiler cannot recognize the workflow, the result has `status: "needs_ai"`, writes no placeholder capability, and returns exact `next.inspectCommand` and `next.createCommand` commands for an AI authoring handoff. Generated workflows are draft browser writes: inspect the contract and script, stop for explicit user confirmation, then run with `tabwright capability run <capabilityId> --browser user --input-json ... --force --confirm <capabilityId> --json`. Use `replay compile` only when the index has already been inspected or generated separately. The generated script should execute the live frontend path, continue from an already-editing page when possible, and return `needs_ai` with page context when validation, DOM drift, or a missing selector blocks the flow.
|
|
1150
|
+
|
|
1151
|
+
**replay index** - before compiling a replay, use `tabwright replay index <replayId> --json` to inspect the compact AI-readable view of the rrweb events. It preserves actions, fields, user annotations, warnings, and selector hints, but replaces bulky page text and interactive-element arrays with counts. Add `--full` only when the AI needs the complete evidence. The raw rrweb recording remains the source evidence; use `--write` only when you want to persist the generated index under `~/.tabwright/replay-ai-indexes`.
|
|
1152
|
+
|
|
1153
|
+
**replay eval** - run the replay productization self-test platform. It creates local example pages, writes synthetic rrweb recordings, builds the AI index, compiles draft capabilities, runs the generated scripts in a real browser, and verifies the page/request result. Use it before changing recording/index/compiler/capability code:
|
|
1154
|
+
|
|
1155
|
+
```bash
|
|
1156
|
+
tabwright replay eval
|
|
1157
|
+
tabwright replay eval --json
|
|
1158
|
+
tabwright replay eval --case zh-list-append --headed
|
|
1159
|
+
tabwright replay eval --report tmp/replay-eval-report.html --keep-artifacts
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
The default suite covers Chinese/English list append flows, already-editing pages, draft restart/continue dialogs, duplicate-value short-circuiting, deleted annotations, page drift returning `needs_ai`, and unsupported replays failing explicitly instead of generating fake automation.
|
|
1163
|
+
|
|
1164
|
+
```js
|
|
1165
|
+
const latestReplay = (await replay.list({ limit: 1 }))[0]
|
|
1166
|
+
|
|
1167
|
+
const saved = workflow.saveFromRecording({
|
|
1168
|
+
id: 'create-material-from-demo',
|
|
1169
|
+
title: 'Create material from demo',
|
|
1170
|
+
description: 'Fill the material form from structured input and stop with needs_ai if the page drifts.',
|
|
1171
|
+
recordingId: latestReplay.id,
|
|
1172
|
+
match: ['https://admin.example.com/*'],
|
|
1173
|
+
inputSchema: {
|
|
1174
|
+
type: 'object',
|
|
1175
|
+
properties: {
|
|
1176
|
+
items: {
|
|
1177
|
+
type: 'array',
|
|
1178
|
+
items: {
|
|
1179
|
+
type: 'object',
|
|
1180
|
+
properties: {
|
|
1181
|
+
title: { type: 'string' },
|
|
1182
|
+
image: { type: 'string' },
|
|
1183
|
+
},
|
|
1184
|
+
required: ['title', 'image'],
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1187
|
+
},
|
|
1188
|
+
required: ['items'],
|
|
1189
|
+
},
|
|
1190
|
+
steps: [
|
|
1191
|
+
{ action: 'goto', url: { value: 'https://admin.example.com/materials/new' } },
|
|
1192
|
+
{ action: 'fill', locator: '[name="title"]', value: { inputPath: 'title' } },
|
|
1193
|
+
{ action: 'setInputFiles', locator: '[name="image"]', path: { inputPath: 'image' } },
|
|
1194
|
+
],
|
|
1195
|
+
finalRequest: {
|
|
1196
|
+
url: '**/api/materials/**',
|
|
1197
|
+
method: 'POST',
|
|
1198
|
+
title: 'Create material',
|
|
1199
|
+
trigger: { action: 'click', locator: 'button[type="submit"]' },
|
|
1200
|
+
},
|
|
1201
|
+
})
|
|
1202
|
+
|
|
1203
|
+
console.log(saved.capability)
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
**ghostCursor.show / ghostCursor.hide** - the ghost cursor overlay is always on: the extension injects it on every Tabwright-attached tab and it stays visible at the last spot Playwright clicked or moved. These methods only matter if you want to change the cursor style or temporarily hide it:
|
|
1207
|
+
|
|
1208
|
+
```js
|
|
1209
|
+
await ghostCursor.show({ page: state.page, style: 'screenstudio' }) // 'minimal' (default), 'dot', 'screenstudio'
|
|
1210
|
+
await ghostCursor.hide({ page: state.page }) // hide until next show() or hard navigation
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
## pinned elements
|
|
1214
|
+
|
|
1215
|
+
Users can right-click → "Copy Tabwright Element Reference" to store elements in `globalThis.tabwrightPinnedElem1` (increments for each pin). The reference is copied to clipboard:
|
|
1216
|
+
|
|
1217
|
+
```js
|
|
1218
|
+
const el = await state.page.evaluateHandle(() => globalThis.tabwrightPinnedElem1)
|
|
1219
|
+
await el.click()
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
## taking screenshots
|
|
1223
|
+
|
|
1224
|
+
Always use `scale: 'css'` to avoid 2-4x larger images on high-DPI displays:
|
|
1225
|
+
|
|
1226
|
+
```js
|
|
1227
|
+
await state.page.screenshot({ path: '/absolute/path/to/shot.png', scale: 'css' })
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
If you want to read back the image file into context, resize it first so it consumes fewer tokens:
|
|
1231
|
+
|
|
1232
|
+
```js
|
|
1233
|
+
await resizeImageForAgent({ input: './shot.png' })
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
## page.evaluate
|
|
1237
|
+
|
|
1238
|
+
Code inside `page.evaluate()` runs in the browser - use plain JavaScript only, no TypeScript syntax. Return values and log outside (console.log inside evaluate runs in browser, not visible):
|
|
1239
|
+
|
|
1240
|
+
```js
|
|
1241
|
+
const title = await state.page.evaluate(() => document.title)
|
|
1242
|
+
console.log('Title:', title)
|
|
1243
|
+
|
|
1244
|
+
const info = await state.page.evaluate(() => ({
|
|
1245
|
+
url: location.href,
|
|
1246
|
+
buttons: document.querySelectorAll('button').length,
|
|
1247
|
+
}))
|
|
1248
|
+
console.log(info)
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
## loading files
|
|
1252
|
+
|
|
1253
|
+
Fill inputs with file content:
|
|
1254
|
+
|
|
1255
|
+
```js
|
|
1256
|
+
const fs = require('node:fs')
|
|
1257
|
+
const content = fs.readFileSync('./data.txt', 'utf-8')
|
|
1258
|
+
await state.page.locator('textarea').fill(content)
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
## network interception
|
|
1262
|
+
|
|
1263
|
+
For scraping or reverse-engineering APIs, intercept network requests instead of scrolling DOM. Store in `state` to analyze across calls:
|
|
1264
|
+
|
|
1265
|
+
```js
|
|
1266
|
+
state.requests = []
|
|
1267
|
+
state.responses = []
|
|
1268
|
+
state.page.on('request', (req) => {
|
|
1269
|
+
if (req.url().includes('/api/')) state.requests.push({ url: req.url(), method: req.method(), headers: req.headers() })
|
|
1270
|
+
})
|
|
1271
|
+
state.page.on('response', async (res) => {
|
|
1272
|
+
if (res.url().includes('/api/')) {
|
|
1273
|
+
try {
|
|
1274
|
+
state.responses.push({ url: res.url(), status: res.status(), body: await res.json() })
|
|
1275
|
+
} catch {}
|
|
1276
|
+
}
|
|
1277
|
+
})
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
Then trigger actions (scroll, click, navigate) and analyze captured data:
|
|
1281
|
+
|
|
1282
|
+
```js
|
|
1283
|
+
console.log('Captured', state.responses.length, 'API calls')
|
|
1284
|
+
state.responses.forEach((r) => console.log(r.status, r.url.slice(0, 80)))
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
Inspect a specific response to understand schema:
|
|
1288
|
+
|
|
1289
|
+
```js
|
|
1290
|
+
const resp = state.responses.find((r) => r.url.includes('users'))
|
|
1291
|
+
console.log(JSON.stringify(resp.body, null, 2).slice(0, 2000))
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
Replay API directly (useful for pagination):
|
|
1295
|
+
|
|
1296
|
+
```js
|
|
1297
|
+
const { url, headers } = state.requests.find((r) => r.url.includes('feed'))
|
|
1298
|
+
const data = await state.page.evaluate(
|
|
1299
|
+
async ({ url, headers }) => {
|
|
1300
|
+
const res = await fetch(url, { headers })
|
|
1301
|
+
return res.json()
|
|
1302
|
+
},
|
|
1303
|
+
{ url, headers },
|
|
1304
|
+
)
|
|
1305
|
+
console.log(data)
|
|
1306
|
+
```
|
|
1307
|
+
|
|
1308
|
+
Clean up listeners when done: `state.page.removeAllListeners('request'); state.page.removeAllListeners('response');`
|
|
1309
|
+
|
|
1310
|
+
## computer use (low-level mouse/keyboard)
|
|
1311
|
+
|
|
1312
|
+
### clicking
|
|
1313
|
+
|
|
1314
|
+
```js
|
|
1315
|
+
// Preferred: by locator (stable, auto-waits, no coordinates needed)
|
|
1316
|
+
await state.page.locator('button[name="Submit"]').click()
|
|
1317
|
+
await state.page.locator('text=Login').click({ button: 'right' })
|
|
1318
|
+
await state.page.locator('text=Login').dblclick()
|
|
1319
|
+
await state.page
|
|
1320
|
+
.locator('a')
|
|
1321
|
+
.first()
|
|
1322
|
+
.click({ modifiers: ['Meta'] }) // cmd+click opens link in new background tab
|
|
1323
|
+
|
|
1324
|
+
// By coordinates (when locators aren't available, e.g. canvas, maps, custom widgets)
|
|
1325
|
+
await state.page.mouse.click(450, 320) // left click
|
|
1326
|
+
await state.page.mouse.click(450, 320, { button: 'right' }) // right click
|
|
1327
|
+
await state.page.mouse.dblclick(450, 320) // double click
|
|
1328
|
+
await state.page.mouse.click(450, 320, { clickCount: 3 }) // triple click
|
|
1329
|
+
await state.page.mouse.click(450, 320, { modifiers: ['Shift'] }) // shift+click
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
### hover
|
|
1333
|
+
|
|
1334
|
+
```js
|
|
1335
|
+
await state.page.locator('.tooltip-trigger').hover() // by locator (preferred)
|
|
1336
|
+
await state.page.mouse.move(450, 320) // by coordinates
|
|
1337
|
+
```
|
|
1338
|
+
|
|
1339
|
+
### scroll
|
|
1340
|
+
|
|
1341
|
+
```js
|
|
1342
|
+
// By locator (preferred)
|
|
1343
|
+
await state.page.locator('#footer').scrollIntoViewIfNeeded()
|
|
1344
|
+
|
|
1345
|
+
// By pixel (for canvas, maps, infinite scroll)
|
|
1346
|
+
await state.page.mouse.wheel(0, 300) // scroll down 300px
|
|
1347
|
+
await state.page.mouse.wheel(0, -300) // scroll up
|
|
1348
|
+
await state.page.mouse.wheel(300, 0) // scroll right
|
|
1349
|
+
await state.page.mouse.wheel(-300, 0) // scroll left
|
|
1350
|
+
|
|
1351
|
+
// Scroll at a specific position
|
|
1352
|
+
await state.page.mouse.move(450, 320)
|
|
1353
|
+
await state.page.mouse.wheel(0, 500)
|
|
1354
|
+
|
|
1355
|
+
// Scroll inside a container
|
|
1356
|
+
await state.page.locator('.scrollable-list').evaluate((el) => {
|
|
1357
|
+
el.scrollTop += 500
|
|
1358
|
+
})
|
|
1359
|
+
```
|
|
1360
|
+
|
|
1361
|
+
### drag
|
|
1362
|
+
|
|
1363
|
+
```js
|
|
1364
|
+
// By locator (preferred)
|
|
1365
|
+
await state.page.locator('#item').dragTo(state.page.locator('#target'))
|
|
1366
|
+
|
|
1367
|
+
// By coordinates (for canvas, sliders, custom drag targets)
|
|
1368
|
+
await state.page.mouse.move(100, 200)
|
|
1369
|
+
await state.page.mouse.down()
|
|
1370
|
+
await state.page.mouse.move(400, 500, { steps: 10 }) // steps for smooth drag
|
|
1371
|
+
await state.page.mouse.up()
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
**Freehand drawing, annotation widgets, and canvas tools** use this same `mouse.down → move → up` pattern. If a widget expects a drawn stroke (paint tools, annotation overlays, range sliders, timeline scrubbers), always use held-mouse motion — not `mouse.click()`:
|
|
1375
|
+
|
|
1376
|
+
```js
|
|
1377
|
+
// Draw a stroke across a canvas or annotation layer
|
|
1378
|
+
await state.page.mouse.move(startX, startY)
|
|
1379
|
+
await state.page.mouse.down()
|
|
1380
|
+
await state.page.mouse.move(endX, endY, { steps: 15 }) // steps = smoother stroke
|
|
1381
|
+
await state.page.mouse.up()
|
|
1382
|
+
await state.page.waitForTimeout(500) // let the widget process the stroke
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
### key hold / release / repeat
|
|
1386
|
+
|
|
1387
|
+
```js
|
|
1388
|
+
// Hold modifier while pressing another key
|
|
1389
|
+
await state.page.keyboard.down('Shift')
|
|
1390
|
+
await state.page.keyboard.press('ArrowDown')
|
|
1391
|
+
await state.page.keyboard.up('Shift')
|
|
1392
|
+
|
|
1393
|
+
// Repeat a key
|
|
1394
|
+
for (let i = 0; i < 5; i++) await state.page.keyboard.press('ArrowDown')
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
### resize viewport
|
|
1398
|
+
|
|
1399
|
+
```js
|
|
1400
|
+
await state.page.setViewportSize({ width: 1280, height: 720 })
|
|
1401
|
+
```
|
|
1402
|
+
|
|
1403
|
+
### region screenshot (zoom equivalent)
|
|
1404
|
+
|
|
1405
|
+
```js
|
|
1406
|
+
await state.page.screenshot({ path: '/absolute/path/to/region.png', scale: 'css', clip: { x: 100, y: 200, width: 400, height: 300 } })
|
|
1407
|
+
```
|
|
1408
|
+
|
|
1409
|
+
Prefer locator-based actions over coordinates — locators are stable across scroll/resize, auto-wait for elements, and don't require screenshot round-trips that burn ~800 image tokens per cycle.
|
|
1410
|
+
|
|
1411
|
+
## Ghost Browser integration
|
|
1412
|
+
|
|
1413
|
+
When running in [Ghost Browser](https://ghostbrowser.com/), the `chrome` object exposes APIs for multi-identity automation (identities, proxies, sessions). See `extension/src/ghost-browser-api.d.ts` for full API reference. Only works in Ghost Browser — calls fail in regular Chrome.
|