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
|
@@ -0,0 +1,1427 @@
|
|
|
1
|
+
import { createMCPClient } from './mcp-client.js';
|
|
2
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
3
|
+
import { chromium } from '@xmorse/playwright-core';
|
|
4
|
+
import { getCDPSessionForPage } from './cdp-session.js';
|
|
5
|
+
import { getCdpUrl, LOG_CDP_FILE_PATH } from './utils.js';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import { setupTestContext, cleanupTestContext, getExtensionServiceWorker, withTimeout, js, tryJsonParse, createSimpleServer, safeCloseCDPBrowser, } from './test-utils.js';
|
|
8
|
+
import './test-declarations.js';
|
|
9
|
+
const TEST_PORT = 19987;
|
|
10
|
+
describe('Relay Core Tests', () => {
|
|
11
|
+
let client;
|
|
12
|
+
let cleanup = null;
|
|
13
|
+
let testCtx = null;
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
testCtx = await setupTestContext({ port: TEST_PORT, tempDirPrefix: 'pw-test-', toggleExtension: true });
|
|
16
|
+
const result = await createMCPClient({ port: TEST_PORT });
|
|
17
|
+
client = result.client;
|
|
18
|
+
cleanup = result.cleanup;
|
|
19
|
+
}, 600000);
|
|
20
|
+
afterAll(async () => {
|
|
21
|
+
await cleanupTestContext(testCtx, cleanup);
|
|
22
|
+
cleanup = null;
|
|
23
|
+
testCtx = null;
|
|
24
|
+
});
|
|
25
|
+
const getBrowserContext = () => {
|
|
26
|
+
if (!testCtx?.browserContext)
|
|
27
|
+
throw new Error('Browser not initialized');
|
|
28
|
+
return testCtx.browserContext;
|
|
29
|
+
};
|
|
30
|
+
const ensureConnectedTabForExecute = async () => {
|
|
31
|
+
const browserContext = getBrowserContext();
|
|
32
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
33
|
+
const connectedTabCount = await serviceWorker.evaluate(async () => {
|
|
34
|
+
const state = globalThis.getExtensionState();
|
|
35
|
+
return state.tabs.size;
|
|
36
|
+
});
|
|
37
|
+
if (connectedTabCount > 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const page = await browserContext.newPage();
|
|
41
|
+
await page.goto('about:blank');
|
|
42
|
+
await page.bringToFront();
|
|
43
|
+
await serviceWorker.evaluate(async () => {
|
|
44
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
45
|
+
});
|
|
46
|
+
await new Promise((r) => {
|
|
47
|
+
setTimeout(r, 100);
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
it('should inject script via addScriptTag through CDP relay', async () => {
|
|
51
|
+
const browserContext = getBrowserContext();
|
|
52
|
+
const serviceWorker = await withTimeout({
|
|
53
|
+
promise: getExtensionServiceWorker(browserContext),
|
|
54
|
+
timeoutMs: 5000,
|
|
55
|
+
errorMessage: 'Timed out waiting for extension service worker for iframe test',
|
|
56
|
+
});
|
|
57
|
+
const page = await browserContext.newPage();
|
|
58
|
+
const html = '<html><body><button id="btn">Click</button></body></html>';
|
|
59
|
+
const dataUrl = `data:text/html,${encodeURIComponent(html)}`;
|
|
60
|
+
await page.goto(dataUrl);
|
|
61
|
+
await page.bringToFront();
|
|
62
|
+
await withTimeout({
|
|
63
|
+
promise: serviceWorker.evaluate(async () => {
|
|
64
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
65
|
+
}),
|
|
66
|
+
timeoutMs: 10000,
|
|
67
|
+
errorMessage: 'Timed out toggling extension for active tab',
|
|
68
|
+
});
|
|
69
|
+
await new Promise((r) => {
|
|
70
|
+
setTimeout(r, 100);
|
|
71
|
+
});
|
|
72
|
+
const cdpSession = await withTimeout({
|
|
73
|
+
promise: getCDPSessionForPage({ page }),
|
|
74
|
+
timeoutMs: 10000,
|
|
75
|
+
errorMessage: 'Timed out creating CDP session for page',
|
|
76
|
+
});
|
|
77
|
+
const hasGlobalBefore = await page.evaluate(() => {
|
|
78
|
+
return Boolean(globalThis.__testGlobal);
|
|
79
|
+
});
|
|
80
|
+
expect(hasGlobalBefore).toBe(false);
|
|
81
|
+
await withTimeout({
|
|
82
|
+
promise: (async () => {
|
|
83
|
+
await cdpSession.send('Page.enable');
|
|
84
|
+
await cdpSession.send('Page.addScriptToEvaluateOnNewDocument', {
|
|
85
|
+
source: 'globalThis.__testGlobal = { foo: "bar" }',
|
|
86
|
+
});
|
|
87
|
+
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
88
|
+
})(),
|
|
89
|
+
timeoutMs: 10000,
|
|
90
|
+
errorMessage: 'Timed out injecting script via CDP session',
|
|
91
|
+
});
|
|
92
|
+
const hasGlobalAfter = await page.evaluate(() => {
|
|
93
|
+
return globalThis.__testGlobal;
|
|
94
|
+
});
|
|
95
|
+
expect(hasGlobalAfter).toEqual({ foo: 'bar' });
|
|
96
|
+
await cdpSession.detach();
|
|
97
|
+
await page.close();
|
|
98
|
+
}, 60000);
|
|
99
|
+
it('should emit download events for both Browser and Page domains in extension mode', async () => {
|
|
100
|
+
const browserContext = getBrowserContext();
|
|
101
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
102
|
+
const logFilePath = LOG_CDP_FILE_PATH;
|
|
103
|
+
const logLineCountBefore = fs.existsSync(logFilePath)
|
|
104
|
+
? fs
|
|
105
|
+
.readFileSync(logFilePath, 'utf-8')
|
|
106
|
+
.split('\n')
|
|
107
|
+
.filter((line) => {
|
|
108
|
+
return line.trim().length > 0;
|
|
109
|
+
}).length
|
|
110
|
+
: 0;
|
|
111
|
+
const server = await createSimpleServer({
|
|
112
|
+
routes: {
|
|
113
|
+
'/': `<!doctype html>
|
|
114
|
+
<html>
|
|
115
|
+
<body>
|
|
116
|
+
<button id="download-button">Download</button>
|
|
117
|
+
<script>
|
|
118
|
+
const button = document.getElementById('download-button');
|
|
119
|
+
button.addEventListener('click', () => {
|
|
120
|
+
const blob = new Blob(['tabwright-download-test'], { type: 'text/plain' });
|
|
121
|
+
const url = URL.createObjectURL(blob);
|
|
122
|
+
const anchor = document.createElement('a');
|
|
123
|
+
anchor.href = url;
|
|
124
|
+
anchor.download = 'tabwright-download-test.txt';
|
|
125
|
+
document.body.appendChild(anchor);
|
|
126
|
+
anchor.click();
|
|
127
|
+
anchor.remove();
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
URL.revokeObjectURL(url);
|
|
130
|
+
}, 1000);
|
|
131
|
+
});
|
|
132
|
+
</script>
|
|
133
|
+
</body>
|
|
134
|
+
</html>`,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
const page = await browserContext.newPage();
|
|
138
|
+
await page.goto(server.baseUrl, { waitUntil: 'domcontentloaded' });
|
|
139
|
+
await page.bringToFront();
|
|
140
|
+
await serviceWorker.evaluate(async () => {
|
|
141
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
142
|
+
});
|
|
143
|
+
const directBrowser = await withTimeout({
|
|
144
|
+
promise: chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
|
|
145
|
+
timeoutMs: 10000,
|
|
146
|
+
errorMessage: 'Timed out connecting over CDP for download reproduction test',
|
|
147
|
+
});
|
|
148
|
+
const connectedPage = directBrowser
|
|
149
|
+
.contexts()[0]
|
|
150
|
+
.pages()
|
|
151
|
+
.find((candidatePage) => {
|
|
152
|
+
return candidatePage.url() === server.baseUrl + '/';
|
|
153
|
+
});
|
|
154
|
+
if (!connectedPage) {
|
|
155
|
+
throw new Error('Connected page not found for download reproduction test');
|
|
156
|
+
}
|
|
157
|
+
const downloadResult = await Promise.all([
|
|
158
|
+
connectedPage.waitForEvent('download', { timeout: 3000 }).then((download) => {
|
|
159
|
+
return { timedOut: false, suggestedFilename: download.suggestedFilename() };
|
|
160
|
+
}, (error) => {
|
|
161
|
+
return { timedOut: true, errorMessage: error.message };
|
|
162
|
+
}),
|
|
163
|
+
connectedPage.click('#download-button'),
|
|
164
|
+
]);
|
|
165
|
+
expect(downloadResult[0]).toMatchInlineSnapshot(`
|
|
166
|
+
{
|
|
167
|
+
"suggestedFilename": "tabwright-download-test.txt",
|
|
168
|
+
"timedOut": false,
|
|
169
|
+
}
|
|
170
|
+
`);
|
|
171
|
+
await directBrowser.close();
|
|
172
|
+
await page.close();
|
|
173
|
+
await server.close();
|
|
174
|
+
const logLinesAfter = fs
|
|
175
|
+
.readFileSync(logFilePath, 'utf-8')
|
|
176
|
+
.split('\n')
|
|
177
|
+
.filter((line) => {
|
|
178
|
+
return line.trim().length > 0;
|
|
179
|
+
})
|
|
180
|
+
.slice(logLineCountBefore);
|
|
181
|
+
const newEntries = logLinesAfter
|
|
182
|
+
.map((line) => {
|
|
183
|
+
return tryJsonParse(line);
|
|
184
|
+
})
|
|
185
|
+
.filter((entry) => {
|
|
186
|
+
return Boolean(entry && typeof entry === 'object' && 'direction' in entry && 'message' in entry);
|
|
187
|
+
});
|
|
188
|
+
const methods = newEntries
|
|
189
|
+
.map((entry) => {
|
|
190
|
+
return {
|
|
191
|
+
direction: entry.direction,
|
|
192
|
+
method: typeof entry.message?.method === 'string' ? entry.message.method : 'response',
|
|
193
|
+
};
|
|
194
|
+
})
|
|
195
|
+
.filter((entry) => {
|
|
196
|
+
return (entry.method.includes('download') ||
|
|
197
|
+
entry.method === 'Browser.setDownloadBehavior' ||
|
|
198
|
+
entry.method === 'Page.setDownloadBehavior');
|
|
199
|
+
});
|
|
200
|
+
const summary = {
|
|
201
|
+
hasBrowserSetDownloadBehavior: methods.some((entry) => {
|
|
202
|
+
return entry.direction === 'from-playwright' && entry.method === 'Browser.setDownloadBehavior';
|
|
203
|
+
}),
|
|
204
|
+
hasPageSetDownloadBehavior: methods.some((entry) => {
|
|
205
|
+
return entry.direction === 'to-extension' && entry.method === 'Page.setDownloadBehavior';
|
|
206
|
+
}),
|
|
207
|
+
hasPageDownloadWillBegin: methods.some((entry) => {
|
|
208
|
+
return entry.method === 'Page.downloadWillBegin';
|
|
209
|
+
}),
|
|
210
|
+
hasPageDownloadProgress: methods.some((entry) => {
|
|
211
|
+
return entry.method === 'Page.downloadProgress';
|
|
212
|
+
}),
|
|
213
|
+
hasBrowserDownloadWillBegin: methods.some((entry) => {
|
|
214
|
+
return entry.method === 'Browser.downloadWillBegin';
|
|
215
|
+
}),
|
|
216
|
+
hasBrowserDownloadProgress: methods.some((entry) => {
|
|
217
|
+
return entry.method === 'Browser.downloadProgress';
|
|
218
|
+
}),
|
|
219
|
+
};
|
|
220
|
+
expect(summary).toMatchInlineSnapshot(`
|
|
221
|
+
{
|
|
222
|
+
"hasBrowserDownloadProgress": true,
|
|
223
|
+
"hasBrowserDownloadWillBegin": true,
|
|
224
|
+
"hasBrowserSetDownloadBehavior": true,
|
|
225
|
+
"hasPageDownloadProgress": true,
|
|
226
|
+
"hasPageDownloadWillBegin": true,
|
|
227
|
+
"hasPageSetDownloadBehavior": true,
|
|
228
|
+
}
|
|
229
|
+
`);
|
|
230
|
+
}, 120000);
|
|
231
|
+
it('should ignore duplicate dialog dismissals from multiple CDP clients', async () => {
|
|
232
|
+
const browserContext = getBrowserContext();
|
|
233
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
234
|
+
const logFilePath = `${process.cwd()}/relay-server.log`;
|
|
235
|
+
const logLineCountBefore = fs.existsSync(logFilePath)
|
|
236
|
+
? fs
|
|
237
|
+
.readFileSync(logFilePath, 'utf-8')
|
|
238
|
+
.split('\n')
|
|
239
|
+
.filter((line) => {
|
|
240
|
+
return line.trim().length > 0;
|
|
241
|
+
}).length
|
|
242
|
+
: 0;
|
|
243
|
+
const server = await createSimpleServer({
|
|
244
|
+
routes: {
|
|
245
|
+
'/': `<!doctype html>
|
|
246
|
+
<html>
|
|
247
|
+
<body>
|
|
248
|
+
<button id="open-dialog">Open dialog</button>
|
|
249
|
+
<script>
|
|
250
|
+
document.getElementById('open-dialog').addEventListener('click', () => {
|
|
251
|
+
alert('shared dialog')
|
|
252
|
+
})
|
|
253
|
+
</script>
|
|
254
|
+
</body>
|
|
255
|
+
</html>`,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
const page = await browserContext.newPage();
|
|
259
|
+
let browserA = null;
|
|
260
|
+
let browserB = null;
|
|
261
|
+
try {
|
|
262
|
+
await page.goto(server.baseUrl, { waitUntil: 'domcontentloaded' });
|
|
263
|
+
await page.bringToFront();
|
|
264
|
+
await serviceWorker.evaluate(async () => {
|
|
265
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
266
|
+
});
|
|
267
|
+
await new Promise((r) => {
|
|
268
|
+
setTimeout(r, 100);
|
|
269
|
+
});
|
|
270
|
+
browserA = await withTimeout({
|
|
271
|
+
promise: chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
|
|
272
|
+
timeoutMs: 10000,
|
|
273
|
+
errorMessage: 'Timed out connecting first CDP client for dialog test',
|
|
274
|
+
});
|
|
275
|
+
browserB = await withTimeout({
|
|
276
|
+
promise: chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
|
|
277
|
+
timeoutMs: 10000,
|
|
278
|
+
errorMessage: 'Timed out connecting second CDP client for dialog test',
|
|
279
|
+
});
|
|
280
|
+
const getConnectedPage = (browser) => {
|
|
281
|
+
return browser
|
|
282
|
+
.contexts()[0]
|
|
283
|
+
?.pages()
|
|
284
|
+
.find((candidatePage) => {
|
|
285
|
+
return candidatePage.url() === server.baseUrl + '/';
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
const connectedPageA = getConnectedPage(browserA);
|
|
289
|
+
const connectedPageB = getConnectedPage(browserB);
|
|
290
|
+
expect(connectedPageA).toBeDefined();
|
|
291
|
+
expect(connectedPageB).toBeDefined();
|
|
292
|
+
await withTimeout({
|
|
293
|
+
promise: connectedPageA.click('#open-dialog'),
|
|
294
|
+
timeoutMs: 5000,
|
|
295
|
+
errorMessage: 'Timed out opening dialog with first CDP client',
|
|
296
|
+
});
|
|
297
|
+
const secondClientResult = await withTimeout({
|
|
298
|
+
promise: connectedPageB.evaluate(() => {
|
|
299
|
+
return 2 + 2;
|
|
300
|
+
}),
|
|
301
|
+
timeoutMs: 5000,
|
|
302
|
+
errorMessage: 'Second CDP client stopped responding after shared dialog dismissal',
|
|
303
|
+
});
|
|
304
|
+
expect(secondClientResult).toBe(4);
|
|
305
|
+
await new Promise((r) => {
|
|
306
|
+
setTimeout(r, 200);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
if (browserA) {
|
|
311
|
+
await safeCloseCDPBrowser(browserA);
|
|
312
|
+
}
|
|
313
|
+
if (browserB) {
|
|
314
|
+
await safeCloseCDPBrowser(browserB);
|
|
315
|
+
}
|
|
316
|
+
await page.close();
|
|
317
|
+
await server.close();
|
|
318
|
+
}
|
|
319
|
+
const logLinesAfter = fs
|
|
320
|
+
.readFileSync(logFilePath, 'utf-8')
|
|
321
|
+
.split('\n')
|
|
322
|
+
.filter((line) => {
|
|
323
|
+
return line.trim().length > 0;
|
|
324
|
+
})
|
|
325
|
+
.slice(logLineCountBefore);
|
|
326
|
+
const unexpectedRelayCrashes = logLinesAfter.filter((line) => {
|
|
327
|
+
return line.includes('Unhandled Rejection:');
|
|
328
|
+
});
|
|
329
|
+
expect(unexpectedRelayCrashes).toEqual([]);
|
|
330
|
+
}, 60000);
|
|
331
|
+
it('should execute code and capture console output', async () => {
|
|
332
|
+
await client.callTool({
|
|
333
|
+
name: 'execute',
|
|
334
|
+
arguments: {
|
|
335
|
+
code: js `
|
|
336
|
+
const newPage = await context.newPage();
|
|
337
|
+
state.page = newPage;
|
|
338
|
+
if (!state.pages) state.pages = [];
|
|
339
|
+
state.pages.push(newPage);
|
|
340
|
+
`,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
const result = await client.callTool({
|
|
344
|
+
name: 'execute',
|
|
345
|
+
arguments: {
|
|
346
|
+
code: js `
|
|
347
|
+
await state.page.goto('https://example.com');
|
|
348
|
+
const title = await state.page.title();
|
|
349
|
+
console.log('Page title:', title);
|
|
350
|
+
return { url: state.page.url(), title };
|
|
351
|
+
`,
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
expect(result.content).toMatchInlineSnapshot(`
|
|
355
|
+
[
|
|
356
|
+
{
|
|
357
|
+
"text": "Console output:
|
|
358
|
+
[log] Page title: Example Domain
|
|
359
|
+
|
|
360
|
+
[return value] { url: 'https://example.com/', title: 'Example Domain' }",
|
|
361
|
+
"type": "text",
|
|
362
|
+
},
|
|
363
|
+
]
|
|
364
|
+
`);
|
|
365
|
+
expect(result.content).toBeDefined();
|
|
366
|
+
}, 30000);
|
|
367
|
+
// Repro test for https://github.com/remorses/playwriter/issues/66.
|
|
368
|
+
// Current limitation: extension-mode routing does not support root-session
|
|
369
|
+
// Storage.getCookies in playwriter. MUST use Network.getCookies via page CDP
|
|
370
|
+
// session instead (see test below), so this repro stays skipped.
|
|
371
|
+
it.skip('should reproduce page.route failure in MCP execute path (issue #66)', async () => {
|
|
372
|
+
const server = await createSimpleServer({
|
|
373
|
+
routes: {
|
|
374
|
+
'/': '<!doctype html><html><body>route issue repro</body></html>',
|
|
375
|
+
'/api/data': '{"ok":true}',
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
try {
|
|
379
|
+
const result = await client.callTool({
|
|
380
|
+
name: 'execute',
|
|
381
|
+
arguments: {
|
|
382
|
+
code: js `
|
|
383
|
+
const newPage = await context.newPage();
|
|
384
|
+
state.issue66Page = newPage;
|
|
385
|
+
await newPage.goto('${server.baseUrl}', { waitUntil: 'domcontentloaded' });
|
|
386
|
+
|
|
387
|
+
let routeFetchError = null;
|
|
388
|
+
await newPage.route('**/api/**', async (route) => {
|
|
389
|
+
try {
|
|
390
|
+
const response = await route.fetch();
|
|
391
|
+
await route.fulfill({ response });
|
|
392
|
+
} catch (error) {
|
|
393
|
+
routeFetchError = error instanceof Error ? error.message : String(error);
|
|
394
|
+
await route.abort();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
await newPage.evaluate(async () => {
|
|
399
|
+
await fetch('/api/data').catch(() => null);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return { routeFetchError };
|
|
403
|
+
`,
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
const resultWithContent = result;
|
|
407
|
+
const content = Array.isArray(resultWithContent.content) ? resultWithContent.content : [];
|
|
408
|
+
const firstContent = content[0];
|
|
409
|
+
const output = typeof firstContent === 'object' && firstContent !== null && 'text' in firstContent
|
|
410
|
+
? String(firstContent.text ?? '')
|
|
411
|
+
: '';
|
|
412
|
+
expect(output).toContain('routeFetchError');
|
|
413
|
+
expect(output).toContain('Storage.getCookies');
|
|
414
|
+
expect(output).toContain('No tab found for method Storage.getCookies');
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
try {
|
|
418
|
+
await client.callTool({
|
|
419
|
+
name: 'execute',
|
|
420
|
+
arguments: {
|
|
421
|
+
code: js `
|
|
422
|
+
if (state.issue66Page && !state.issue66Page.isClosed()) {
|
|
423
|
+
await state.issue66Page.close();
|
|
424
|
+
}
|
|
425
|
+
delete state.issue66Page;
|
|
426
|
+
`,
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
// Ignore cleanup failure if MCP disconnected due to the repro.
|
|
432
|
+
}
|
|
433
|
+
await server.close();
|
|
434
|
+
}
|
|
435
|
+
}, 30000);
|
|
436
|
+
it('should read cookies via Network.getCookies through page CDP session', async () => {
|
|
437
|
+
const browserContext = getBrowserContext();
|
|
438
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
439
|
+
const server = await createSimpleServer({
|
|
440
|
+
routes: {
|
|
441
|
+
'/': '<!doctype html><html><body>cookies test</body></html>',
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
const page = await browserContext.newPage();
|
|
445
|
+
try {
|
|
446
|
+
await page.goto(server.baseUrl, { waitUntil: 'domcontentloaded' });
|
|
447
|
+
await page.bringToFront();
|
|
448
|
+
await serviceWorker.evaluate(async () => {
|
|
449
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
450
|
+
});
|
|
451
|
+
await new Promise((r) => {
|
|
452
|
+
setTimeout(r, 200);
|
|
453
|
+
});
|
|
454
|
+
await page.evaluate(() => {
|
|
455
|
+
document.cookie = 'issue66=ok; path=/';
|
|
456
|
+
});
|
|
457
|
+
const cdpSession = await getCDPSessionForPage({ page });
|
|
458
|
+
const cookiesResult = await cdpSession.send('Network.getCookies', { urls: [page.url()] });
|
|
459
|
+
const cookie = cookiesResult.cookies.find((value) => {
|
|
460
|
+
return value.name === 'issue66';
|
|
461
|
+
});
|
|
462
|
+
expect(cookie?.value).toBe('ok');
|
|
463
|
+
}
|
|
464
|
+
finally {
|
|
465
|
+
await page.close();
|
|
466
|
+
await server.close();
|
|
467
|
+
}
|
|
468
|
+
}, 30000);
|
|
469
|
+
it('should show extension as connected for pages created via newPage()', async () => {
|
|
470
|
+
const browserContext = getBrowserContext();
|
|
471
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
472
|
+
// Create a page via MCP (which uses context.newPage())
|
|
473
|
+
await client.callTool({
|
|
474
|
+
name: 'execute',
|
|
475
|
+
arguments: {
|
|
476
|
+
code: js `
|
|
477
|
+
const newPage = await context.newPage();
|
|
478
|
+
state.testPage = newPage;
|
|
479
|
+
await newPage.goto('https://example.com/mcp-test');
|
|
480
|
+
return newPage.url();
|
|
481
|
+
`,
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
// Get extension state to verify the page is marked as connected
|
|
485
|
+
const extensionState = await serviceWorker.evaluate(async () => {
|
|
486
|
+
const state = globalThis.getExtensionState();
|
|
487
|
+
const tabs = await chrome.tabs.query({});
|
|
488
|
+
const testTab = tabs.find((t) => t.url?.includes('mcp-test'));
|
|
489
|
+
return {
|
|
490
|
+
connected: !!testTab && !!testTab.id && state.tabs.has(testTab.id),
|
|
491
|
+
tabId: testTab?.id,
|
|
492
|
+
tabInfo: testTab?.id ? state.tabs.get(testTab.id) : null,
|
|
493
|
+
connectionState: state.connectionState,
|
|
494
|
+
};
|
|
495
|
+
});
|
|
496
|
+
expect(extensionState.connected).toBe(true);
|
|
497
|
+
expect(extensionState.tabInfo?.state).toBe('connected');
|
|
498
|
+
expect(extensionState.connectionState).toBe('connected');
|
|
499
|
+
// Clean up
|
|
500
|
+
await client.callTool({
|
|
501
|
+
name: 'execute',
|
|
502
|
+
arguments: {
|
|
503
|
+
code: js `
|
|
504
|
+
if (state.testPage) {
|
|
505
|
+
await state.testPage.close();
|
|
506
|
+
delete state.testPage;
|
|
507
|
+
}
|
|
508
|
+
`,
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
}, 30000);
|
|
512
|
+
const snapshotTestCases = [
|
|
513
|
+
{
|
|
514
|
+
name: 'hacker-news',
|
|
515
|
+
url: 'https://news.ycombinator.com/item?id=1',
|
|
516
|
+
expectedContent: ['role=link', 'Hacker News'],
|
|
517
|
+
waitForCode: js `
|
|
518
|
+
await state.page.locator('a[href="news"]').first().waitFor({ timeout: 10000 });
|
|
519
|
+
await state.page.locator('a.hnuser').first().waitFor({ timeout: 10000 });
|
|
520
|
+
`,
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
name: 'shadcn-ui',
|
|
524
|
+
url: 'https://ui.shadcn.com/',
|
|
525
|
+
expectedContent: ['shadcn'],
|
|
526
|
+
waitForCode: js `
|
|
527
|
+
await state.page.getByRole('heading', { name: 'The Foundation for your Design System' }).waitFor({ timeout: 10000 });
|
|
528
|
+
`,
|
|
529
|
+
},
|
|
530
|
+
];
|
|
531
|
+
for (const testCase of snapshotTestCases) {
|
|
532
|
+
it(`should get accessibility snapshot of ${testCase.name}`, async () => {
|
|
533
|
+
await client.callTool({
|
|
534
|
+
name: 'execute',
|
|
535
|
+
arguments: {
|
|
536
|
+
code: js `
|
|
537
|
+
const newPage = await context.newPage();
|
|
538
|
+
state.page = newPage;
|
|
539
|
+
if (!state.pages) state.pages = [];
|
|
540
|
+
state.pages.push(newPage);
|
|
541
|
+
`,
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
// Capture interactiveOnly=true snapshot (default)
|
|
545
|
+
const interactiveResult = await client.callTool({
|
|
546
|
+
name: 'execute',
|
|
547
|
+
arguments: {
|
|
548
|
+
code: js `
|
|
549
|
+
// External pages can expose a partial AX tree right after domcontentloaded,
|
|
550
|
+
// so wait for stable page-specific content before snapshotting.
|
|
551
|
+
await state.page.goto('${testCase.url}', { waitUntil: 'domcontentloaded' });
|
|
552
|
+
${testCase.waitForCode}
|
|
553
|
+
const snap = await snapshot({ page: state.page, showDiffSinceLastCall: false, interactiveOnly: true });
|
|
554
|
+
return snap;
|
|
555
|
+
`,
|
|
556
|
+
timeout: 30000,
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
const interactiveData = typeof interactiveResult === 'object' && interactiveResult.content?.[0]?.text
|
|
560
|
+
? tryJsonParse(interactiveResult.content[0].text)
|
|
561
|
+
: interactiveResult;
|
|
562
|
+
await expect(interactiveData).toMatchFileSnapshot(`snapshots/${testCase.name}-accessibility-interactive.md`);
|
|
563
|
+
expect(interactiveResult.content).toBeDefined();
|
|
564
|
+
for (const expected of testCase.expectedContent) {
|
|
565
|
+
expect(interactiveData).toContain(expected);
|
|
566
|
+
}
|
|
567
|
+
// Capture interactiveOnly=false snapshot (full tree)
|
|
568
|
+
const fullResult = await client.callTool({
|
|
569
|
+
name: 'execute',
|
|
570
|
+
arguments: {
|
|
571
|
+
code: js `
|
|
572
|
+
const snap = await snapshot({ page: state.page, showDiffSinceLastCall: false, interactiveOnly: false });
|
|
573
|
+
return snap;
|
|
574
|
+
`,
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
const fullData = typeof fullResult === 'object' && fullResult.content?.[0]?.text
|
|
578
|
+
? tryJsonParse(fullResult.content[0].text)
|
|
579
|
+
: fullResult;
|
|
580
|
+
await expect(fullData).toMatchFileSnapshot(`snapshots/${testCase.name}-accessibility-full.md`);
|
|
581
|
+
expect(fullResult.content).toBeDefined();
|
|
582
|
+
for (const expected of testCase.expectedContent) {
|
|
583
|
+
expect(fullData).toContain(expected);
|
|
584
|
+
}
|
|
585
|
+
}, 60000);
|
|
586
|
+
}
|
|
587
|
+
it('should close all created pages', async () => {
|
|
588
|
+
const result = await client.callTool({
|
|
589
|
+
name: 'execute',
|
|
590
|
+
arguments: {
|
|
591
|
+
code: js `
|
|
592
|
+
if (state.pages && state.pages.length > 0) {
|
|
593
|
+
for (const page of state.pages) {
|
|
594
|
+
await page.close();
|
|
595
|
+
}
|
|
596
|
+
const closedCount = state.pages.length;
|
|
597
|
+
state.pages = [];
|
|
598
|
+
return { closedCount };
|
|
599
|
+
}
|
|
600
|
+
return { closedCount: 0 };
|
|
601
|
+
`,
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
it('should capture browser console logs with getLatestLogs', async () => {
|
|
606
|
+
// Ensure clean state and clear any existing logs
|
|
607
|
+
const resetResult = await client.callTool({
|
|
608
|
+
name: 'execute',
|
|
609
|
+
arguments: {
|
|
610
|
+
code: js `
|
|
611
|
+
// Clear any existing logs from previous tests
|
|
612
|
+
clearAllLogs();
|
|
613
|
+
console.log('Cleared all existing logs');
|
|
614
|
+
|
|
615
|
+
// Verify connection is working
|
|
616
|
+
const pages = context.pages();
|
|
617
|
+
console.log('Current pages count:', pages.length);
|
|
618
|
+
|
|
619
|
+
return { success: true, pagesCount: pages.length };
|
|
620
|
+
`,
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
console.log('Cleanup result:', resetResult);
|
|
624
|
+
// Create a new page for this test
|
|
625
|
+
await client.callTool({
|
|
626
|
+
name: 'execute',
|
|
627
|
+
arguments: {
|
|
628
|
+
code: js `
|
|
629
|
+
const newPage = await context.newPage();
|
|
630
|
+
state.testLogPage = newPage;
|
|
631
|
+
await newPage.goto('about:blank');
|
|
632
|
+
`,
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
// Generate some console logs in the browser
|
|
636
|
+
await client.callTool({
|
|
637
|
+
name: 'execute',
|
|
638
|
+
arguments: {
|
|
639
|
+
code: js `
|
|
640
|
+
await state.testLogPage.evaluate(() => {
|
|
641
|
+
console.log('Test log 12345');
|
|
642
|
+
console.error('Test error 67890');
|
|
643
|
+
console.warn('Test warning 11111');
|
|
644
|
+
console.log('Test log 2 with', { data: 'object' });
|
|
645
|
+
setTimeout(() => { throw new Error('Test pageerror 22222'); }, 0);
|
|
646
|
+
});
|
|
647
|
+
// Wait for logs to be captured
|
|
648
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
649
|
+
`,
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
// Test getting all logs
|
|
653
|
+
const allLogsResult = await client.callTool({
|
|
654
|
+
name: 'execute',
|
|
655
|
+
arguments: {
|
|
656
|
+
code: js `
|
|
657
|
+
const logs = await getLatestLogs();
|
|
658
|
+
logs.forEach(log => console.log(log));
|
|
659
|
+
`,
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
const output = allLogsResult.content[0].text;
|
|
663
|
+
expect(output).toContain('[log] Test log 12345');
|
|
664
|
+
expect(output).toContain('[error] Test error 67890');
|
|
665
|
+
expect(output).toContain('[warning] Test warning 11111');
|
|
666
|
+
expect(output).toContain('[pageerror] Test pageerror 22222');
|
|
667
|
+
// Test filtering by search string
|
|
668
|
+
const errorLogsResult = await client.callTool({
|
|
669
|
+
name: 'execute',
|
|
670
|
+
arguments: {
|
|
671
|
+
code: js `
|
|
672
|
+
const logs = await getLatestLogs({ search: 'error' });
|
|
673
|
+
logs.forEach(log => console.log(log));
|
|
674
|
+
`,
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
const errorOutput = errorLogsResult.content[0].text;
|
|
678
|
+
expect(errorOutput).toContain('[error] Test error 67890');
|
|
679
|
+
// With context lines (5 above/below), nearby logs are also included
|
|
680
|
+
expect(errorOutput).toContain('[log] Test log 12345');
|
|
681
|
+
// Test that logs persist across page reload
|
|
682
|
+
await client.callTool({
|
|
683
|
+
name: 'execute',
|
|
684
|
+
arguments: {
|
|
685
|
+
code: js `
|
|
686
|
+
// First add a log before reload
|
|
687
|
+
await state.testLogPage.evaluate(() => {
|
|
688
|
+
console.log('Before reload 99999');
|
|
689
|
+
});
|
|
690
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
691
|
+
`,
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
// Verify the log exists
|
|
695
|
+
const beforeReloadResult = await client.callTool({
|
|
696
|
+
name: 'execute',
|
|
697
|
+
arguments: {
|
|
698
|
+
code: js `
|
|
699
|
+
const logs = await getLatestLogs({ page: state.testLogPage });
|
|
700
|
+
console.log('Logs before reload:', logs.length);
|
|
701
|
+
logs.forEach(log => console.log(log));
|
|
702
|
+
`,
|
|
703
|
+
},
|
|
704
|
+
});
|
|
705
|
+
const beforeReloadOutput = beforeReloadResult.content[0].text;
|
|
706
|
+
expect(beforeReloadOutput).toContain('[log] Before reload 99999');
|
|
707
|
+
// Reload the page
|
|
708
|
+
await client.callTool({
|
|
709
|
+
name: 'execute',
|
|
710
|
+
arguments: {
|
|
711
|
+
code: js `
|
|
712
|
+
await state.testLogPage.reload();
|
|
713
|
+
await state.testLogPage.evaluate(() => {
|
|
714
|
+
console.log('After reload 88888');
|
|
715
|
+
});
|
|
716
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
717
|
+
`,
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
// Check logs after reload - old logs persist (no longer cleared on navigation)
|
|
721
|
+
// so both pre- and post-reload logs are present. Use sinceLastCall to get
|
|
722
|
+
// only new logs if needed.
|
|
723
|
+
const afterReloadResult = await client.callTool({
|
|
724
|
+
name: 'execute',
|
|
725
|
+
arguments: {
|
|
726
|
+
code: js `
|
|
727
|
+
const logs = await getLatestLogs({ page: state.testLogPage });
|
|
728
|
+
console.log('Logs after reload:', logs.length);
|
|
729
|
+
logs.forEach(log => console.log(log));
|
|
730
|
+
`,
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
const afterReloadOutput = afterReloadResult.content[0].text;
|
|
734
|
+
expect(afterReloadOutput).toContain('[log] After reload 88888');
|
|
735
|
+
expect(afterReloadOutput).toContain('[log] Before reload 99999');
|
|
736
|
+
// Clean up
|
|
737
|
+
await client.callTool({
|
|
738
|
+
name: 'execute',
|
|
739
|
+
arguments: {
|
|
740
|
+
code: js `
|
|
741
|
+
await state.testLogPage.close();
|
|
742
|
+
delete state.testLogPage;
|
|
743
|
+
`,
|
|
744
|
+
},
|
|
745
|
+
});
|
|
746
|
+
}, 30000);
|
|
747
|
+
it('should keep logs separate between different pages', async () => {
|
|
748
|
+
// Clear any existing logs from previous tests
|
|
749
|
+
await client.callTool({
|
|
750
|
+
name: 'execute',
|
|
751
|
+
arguments: {
|
|
752
|
+
code: js `
|
|
753
|
+
clearAllLogs();
|
|
754
|
+
console.log('Cleared all existing logs for second log test');
|
|
755
|
+
`,
|
|
756
|
+
},
|
|
757
|
+
});
|
|
758
|
+
// Create two pages
|
|
759
|
+
await client.callTool({
|
|
760
|
+
name: 'execute',
|
|
761
|
+
arguments: {
|
|
762
|
+
code: js `
|
|
763
|
+
state.pageA = await context.newPage();
|
|
764
|
+
state.pageB = await context.newPage();
|
|
765
|
+
await state.pageA.goto('about:blank');
|
|
766
|
+
await state.pageB.goto('about:blank');
|
|
767
|
+
`,
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
// Generate logs in page A
|
|
771
|
+
await client.callTool({
|
|
772
|
+
name: 'execute',
|
|
773
|
+
arguments: {
|
|
774
|
+
code: js `
|
|
775
|
+
await state.pageA.evaluate(() => {
|
|
776
|
+
console.log('PageA log 11111');
|
|
777
|
+
console.error('PageA error 22222');
|
|
778
|
+
});
|
|
779
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
780
|
+
`,
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
// Generate logs in page B
|
|
784
|
+
await client.callTool({
|
|
785
|
+
name: 'execute',
|
|
786
|
+
arguments: {
|
|
787
|
+
code: js `
|
|
788
|
+
await state.pageB.evaluate(() => {
|
|
789
|
+
console.log('PageB log 33333');
|
|
790
|
+
console.error('PageB error 44444');
|
|
791
|
+
});
|
|
792
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
793
|
+
`,
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
// Check logs for page A - should only have page A logs
|
|
797
|
+
const pageALogsResult = await client.callTool({
|
|
798
|
+
name: 'execute',
|
|
799
|
+
arguments: {
|
|
800
|
+
code: js `
|
|
801
|
+
const logs = await getLatestLogs({ page: state.pageA });
|
|
802
|
+
console.log('Page A logs:', logs.length);
|
|
803
|
+
logs.forEach(log => console.log(log));
|
|
804
|
+
`,
|
|
805
|
+
},
|
|
806
|
+
});
|
|
807
|
+
const pageAOutput = pageALogsResult.content[0].text;
|
|
808
|
+
expect(pageAOutput).toContain('[log] PageA log 11111');
|
|
809
|
+
expect(pageAOutput).toContain('[error] PageA error 22222');
|
|
810
|
+
expect(pageAOutput).not.toContain('PageB');
|
|
811
|
+
// Check logs for page B - should only have page B logs
|
|
812
|
+
const pageBLogsResult = await client.callTool({
|
|
813
|
+
name: 'execute',
|
|
814
|
+
arguments: {
|
|
815
|
+
code: js `
|
|
816
|
+
const logs = await getLatestLogs({ page: state.pageB });
|
|
817
|
+
console.log('Page B logs:', logs.length);
|
|
818
|
+
logs.forEach(log => console.log(log));
|
|
819
|
+
`,
|
|
820
|
+
},
|
|
821
|
+
});
|
|
822
|
+
const pageBOutput = pageBLogsResult.content[0].text;
|
|
823
|
+
expect(pageBOutput).toContain('[log] PageB log 33333');
|
|
824
|
+
expect(pageBOutput).toContain('[error] PageB error 44444');
|
|
825
|
+
expect(pageBOutput).not.toContain('PageA');
|
|
826
|
+
// Check all logs - should have logs from both pages
|
|
827
|
+
const allLogsResult = await client.callTool({
|
|
828
|
+
name: 'execute',
|
|
829
|
+
arguments: {
|
|
830
|
+
code: js `
|
|
831
|
+
const logs = await getLatestLogs();
|
|
832
|
+
console.log('All logs:', logs.length);
|
|
833
|
+
logs.forEach(log => console.log(log));
|
|
834
|
+
`,
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
const allOutput = allLogsResult.content[0].text;
|
|
838
|
+
expect(allOutput).toContain('[log] PageA log 11111');
|
|
839
|
+
expect(allOutput).toContain('[log] PageB log 33333');
|
|
840
|
+
// Test that reloading page A preserves logs (no longer cleared on navigation)
|
|
841
|
+
await client.callTool({
|
|
842
|
+
name: 'execute',
|
|
843
|
+
arguments: {
|
|
844
|
+
code: js `
|
|
845
|
+
await state.pageA.reload();
|
|
846
|
+
await state.pageA.evaluate(() => {
|
|
847
|
+
console.log('PageA after reload 55555');
|
|
848
|
+
});
|
|
849
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
850
|
+
`,
|
|
851
|
+
},
|
|
852
|
+
});
|
|
853
|
+
// Check page A logs - logs persist across navigation, so both old and new are present
|
|
854
|
+
const pageAAfterReloadResult = await client.callTool({
|
|
855
|
+
name: 'execute',
|
|
856
|
+
arguments: {
|
|
857
|
+
code: js `
|
|
858
|
+
const logs = await getLatestLogs({ page: state.pageA });
|
|
859
|
+
console.log('Page A logs after reload:', logs.length);
|
|
860
|
+
logs.forEach(log => console.log(log));
|
|
861
|
+
`,
|
|
862
|
+
},
|
|
863
|
+
});
|
|
864
|
+
const pageAAfterReloadOutput = pageAAfterReloadResult.content[0].text;
|
|
865
|
+
expect(pageAAfterReloadOutput).toContain('[log] PageA after reload 55555');
|
|
866
|
+
expect(pageAAfterReloadOutput).toContain('[log] PageA log 11111');
|
|
867
|
+
// Check page B logs - should still have original logs
|
|
868
|
+
const pageBAfterAReloadResult = await client.callTool({
|
|
869
|
+
name: 'execute',
|
|
870
|
+
arguments: {
|
|
871
|
+
code: js `
|
|
872
|
+
const logs = await getLatestLogs({ page: state.pageB });
|
|
873
|
+
console.log('Page B logs after A reload:', logs.length);
|
|
874
|
+
logs.forEach(log => console.log(log));
|
|
875
|
+
`,
|
|
876
|
+
},
|
|
877
|
+
});
|
|
878
|
+
const pageBAfterAReloadOutput = pageBAfterAReloadResult.content[0].text;
|
|
879
|
+
expect(pageBAfterAReloadOutput).toContain('[log] PageB log 33333');
|
|
880
|
+
expect(pageBAfterAReloadOutput).toContain('[error] PageB error 44444');
|
|
881
|
+
// Test that logs are deleted when page is closed
|
|
882
|
+
await client.callTool({
|
|
883
|
+
name: 'execute',
|
|
884
|
+
arguments: {
|
|
885
|
+
code: js `
|
|
886
|
+
// Close page A
|
|
887
|
+
await state.pageA.close();
|
|
888
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
889
|
+
`,
|
|
890
|
+
},
|
|
891
|
+
});
|
|
892
|
+
// Check all logs - page A logs should be gone
|
|
893
|
+
const logsAfterCloseResult = await client.callTool({
|
|
894
|
+
name: 'execute',
|
|
895
|
+
arguments: {
|
|
896
|
+
code: js `
|
|
897
|
+
const logs = await getLatestLogs();
|
|
898
|
+
console.log('All logs after closing page A:', logs.length);
|
|
899
|
+
logs.forEach(log => console.log(log));
|
|
900
|
+
`,
|
|
901
|
+
},
|
|
902
|
+
});
|
|
903
|
+
const logsAfterCloseOutput = logsAfterCloseResult.content[0].text;
|
|
904
|
+
expect(logsAfterCloseOutput).not.toContain('PageA');
|
|
905
|
+
expect(logsAfterCloseOutput).toContain('[log] PageB log 33333');
|
|
906
|
+
// Clean up remaining page
|
|
907
|
+
await client.callTool({
|
|
908
|
+
name: 'execute',
|
|
909
|
+
arguments: {
|
|
910
|
+
code: js `
|
|
911
|
+
await state.pageB.close();
|
|
912
|
+
delete state.pageA;
|
|
913
|
+
delete state.pageB;
|
|
914
|
+
`,
|
|
915
|
+
},
|
|
916
|
+
});
|
|
917
|
+
}, 30000);
|
|
918
|
+
it('should capture console logs from cross-origin iframes', async () => {
|
|
919
|
+
// Two servers on different ports = different origins
|
|
920
|
+
const iframeServer = await createSimpleServer({
|
|
921
|
+
routes: {
|
|
922
|
+
'/iframe.html': `<!doctype html><html><body>
|
|
923
|
+
<script>
|
|
924
|
+
console.log('iframe-log-ALPHA');
|
|
925
|
+
console.error('iframe-error-BETA');
|
|
926
|
+
console.warn('iframe-warn-GAMMA');
|
|
927
|
+
</script>
|
|
928
|
+
<p>cross-origin iframe</p>
|
|
929
|
+
</body></html>`,
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
const parentServer = await createSimpleServer({
|
|
933
|
+
routes: {
|
|
934
|
+
'/': `<!doctype html><html><body>
|
|
935
|
+
<script>console.log('parent-log-DELTA');</script>
|
|
936
|
+
<iframe src="${iframeServer.baseUrl}/iframe.html"></iframe>
|
|
937
|
+
</body></html>`,
|
|
938
|
+
},
|
|
939
|
+
});
|
|
940
|
+
try {
|
|
941
|
+
// Clear logs and navigate to the parent page with cross-origin iframe
|
|
942
|
+
await client.callTool({
|
|
943
|
+
name: 'execute',
|
|
944
|
+
arguments: {
|
|
945
|
+
code: js `
|
|
946
|
+
clearAllLogs();
|
|
947
|
+
state.iframePage = await context.newPage();
|
|
948
|
+
await state.iframePage.goto('${parentServer.baseUrl}', { waitUntil: 'networkidle' });
|
|
949
|
+
// Wait for iframe to load and logs to be captured
|
|
950
|
+
await state.iframePage.frameLocator('iframe').locator('p').waitFor({ timeout: 5000 });
|
|
951
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
952
|
+
`,
|
|
953
|
+
},
|
|
954
|
+
});
|
|
955
|
+
// Retrieve logs and verify both parent and iframe logs are captured
|
|
956
|
+
const logsResult = await client.callTool({
|
|
957
|
+
name: 'execute',
|
|
958
|
+
arguments: {
|
|
959
|
+
code: js `
|
|
960
|
+
const logs = await getLatestLogs({ page: state.iframePage });
|
|
961
|
+
console.log('Cross-origin iframe logs count:', logs.length);
|
|
962
|
+
logs.forEach(log => console.log(log));
|
|
963
|
+
`,
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
const output = logsResult.content[0].text;
|
|
967
|
+
// Parent page log
|
|
968
|
+
expect(output).toContain('parent-log-DELTA');
|
|
969
|
+
// Cross-origin iframe logs
|
|
970
|
+
expect(output).toContain('iframe-log-ALPHA');
|
|
971
|
+
expect(output).toContain('iframe-error-BETA');
|
|
972
|
+
expect(output).toContain('iframe-warn-GAMMA');
|
|
973
|
+
// Clean up
|
|
974
|
+
await client.callTool({
|
|
975
|
+
name: 'execute',
|
|
976
|
+
arguments: {
|
|
977
|
+
code: js `
|
|
978
|
+
await state.iframePage.close();
|
|
979
|
+
delete state.iframePage;
|
|
980
|
+
`,
|
|
981
|
+
},
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
finally {
|
|
985
|
+
await Promise.all([parentServer.close(), iframeServer.close()]);
|
|
986
|
+
}
|
|
987
|
+
}, 60000);
|
|
988
|
+
it('should preserve system color scheme instead of forcing light mode', async () => {
|
|
989
|
+
const browserContext = getBrowserContext();
|
|
990
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
991
|
+
const page = await browserContext.newPage();
|
|
992
|
+
await page.goto('https://example.com');
|
|
993
|
+
await page.bringToFront();
|
|
994
|
+
// test-utils launches with colorScheme: 'dark', so before MCP connection
|
|
995
|
+
// the browser should report dark mode
|
|
996
|
+
const colorSchemeBefore = await page.evaluate(() => {
|
|
997
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
998
|
+
});
|
|
999
|
+
expect(colorSchemeBefore).toBe('dark');
|
|
1000
|
+
await serviceWorker.evaluate(async () => {
|
|
1001
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
1002
|
+
});
|
|
1003
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1004
|
+
const result = await client.callTool({
|
|
1005
|
+
name: 'execute',
|
|
1006
|
+
arguments: {
|
|
1007
|
+
code: js `
|
|
1008
|
+
const pages = context.pages();
|
|
1009
|
+
const urls = pages.map(p => p.url());
|
|
1010
|
+
const targetPage = pages.find(p => p.url().includes('example.com'));
|
|
1011
|
+
if (!targetPage) {
|
|
1012
|
+
return { error: 'Page not found', urls };
|
|
1013
|
+
}
|
|
1014
|
+
const isDark = await targetPage.evaluate(() => window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
1015
|
+
const isLight = await targetPage.evaluate(() => window.matchMedia('(prefers-color-scheme: light)').matches);
|
|
1016
|
+
return { matchesDark: isDark, matchesLight: isLight };
|
|
1017
|
+
`,
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
1020
|
+
console.log('Color scheme after MCP connection:', result.content);
|
|
1021
|
+
// After MCP connection, color scheme should NOT be forced to light.
|
|
1022
|
+
// The page.ts default is now 'no-override', so the browser's actual
|
|
1023
|
+
// color scheme (dark, from test-utils launch config) should be preserved.
|
|
1024
|
+
expect(result.content).toMatchInlineSnapshot(`
|
|
1025
|
+
[
|
|
1026
|
+
{
|
|
1027
|
+
"text": "[return value] { matchesDark: true, matchesLight: false }",
|
|
1028
|
+
"type": "text",
|
|
1029
|
+
},
|
|
1030
|
+
]
|
|
1031
|
+
`);
|
|
1032
|
+
await page.close();
|
|
1033
|
+
}, 60000);
|
|
1034
|
+
it('should get clean HTML with getCleanHTML', async () => {
|
|
1035
|
+
const browserContext = getBrowserContext();
|
|
1036
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1037
|
+
const page = await browserContext.newPage();
|
|
1038
|
+
await page.setContent(`
|
|
1039
|
+
<html>
|
|
1040
|
+
<head>
|
|
1041
|
+
<style>.hidden { display: none; }</style>
|
|
1042
|
+
<script>console.log('test')</script>
|
|
1043
|
+
</head>
|
|
1044
|
+
<body>
|
|
1045
|
+
<div class="container" data-testid="main">
|
|
1046
|
+
<h1>Hello World</h1>
|
|
1047
|
+
<button id="btn" aria-label="Click me">Submit</button>
|
|
1048
|
+
<a href="/about" title="About page">About</a>
|
|
1049
|
+
<input type="text" placeholder="Enter name" />
|
|
1050
|
+
</div>
|
|
1051
|
+
</body>
|
|
1052
|
+
</html>
|
|
1053
|
+
`);
|
|
1054
|
+
await page.bringToFront();
|
|
1055
|
+
await serviceWorker.evaluate(async () => {
|
|
1056
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
1057
|
+
});
|
|
1058
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
1059
|
+
// Test basic getCleanHTML
|
|
1060
|
+
const result = await client.callTool({
|
|
1061
|
+
name: 'execute',
|
|
1062
|
+
arguments: {
|
|
1063
|
+
code: js `
|
|
1064
|
+
let testPage;
|
|
1065
|
+
for (const p of context.pages()) {
|
|
1066
|
+
const html = await p.content();
|
|
1067
|
+
if (html.includes('Hello World')) { testPage = p; break; }
|
|
1068
|
+
}
|
|
1069
|
+
if (!testPage) throw new Error('Test page not found');
|
|
1070
|
+
const html = await getCleanHTML({ locator: testPage.locator('body') });
|
|
1071
|
+
return html;
|
|
1072
|
+
`,
|
|
1073
|
+
timeout: 15000,
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
1076
|
+
expect(result.isError).toBeFalsy();
|
|
1077
|
+
const text = result.content[0]?.text || '';
|
|
1078
|
+
// Inline snapshot of cleaned HTML
|
|
1079
|
+
expect(text).toMatchInlineSnapshot(`
|
|
1080
|
+
"[return value] <div data-testid="main">
|
|
1081
|
+
<h1>Hello World</h1>
|
|
1082
|
+
<button aria-label="Click me">Submit</button>
|
|
1083
|
+
<a href="/about" title="About page">About</a>
|
|
1084
|
+
<input type="text" placeholder="Enter name">
|
|
1085
|
+
</div>"
|
|
1086
|
+
`);
|
|
1087
|
+
// Should NOT contain script/style tags (they're removed)
|
|
1088
|
+
expect(text).not.toContain('<script');
|
|
1089
|
+
expect(text).not.toContain('<style');
|
|
1090
|
+
expect(text).not.toContain('console.log');
|
|
1091
|
+
// Test search functionality
|
|
1092
|
+
const searchResult = await client.callTool({
|
|
1093
|
+
name: 'execute',
|
|
1094
|
+
arguments: {
|
|
1095
|
+
code: js `
|
|
1096
|
+
let testPage;
|
|
1097
|
+
for (const p of context.pages()) {
|
|
1098
|
+
const html = await p.content();
|
|
1099
|
+
if (html.includes('Hello World')) { testPage = p; break; }
|
|
1100
|
+
}
|
|
1101
|
+
if (!testPage) throw new Error('Test page not found');
|
|
1102
|
+
const html = await getCleanHTML({ locator: testPage, search: /button/i });
|
|
1103
|
+
return html;
|
|
1104
|
+
`,
|
|
1105
|
+
timeout: 15000,
|
|
1106
|
+
},
|
|
1107
|
+
});
|
|
1108
|
+
expect(searchResult.isError).toBeFalsy();
|
|
1109
|
+
const searchText = searchResult.content[0]?.text || '';
|
|
1110
|
+
expect(searchText).toContain('button');
|
|
1111
|
+
await page.close();
|
|
1112
|
+
}, 60000);
|
|
1113
|
+
it('should extract page content as markdown with getPageMarkdown', async () => {
|
|
1114
|
+
const browserContext = getBrowserContext();
|
|
1115
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1116
|
+
const page = await browserContext.newPage();
|
|
1117
|
+
// Create a realistic article-like page structure
|
|
1118
|
+
await page.setContent(`
|
|
1119
|
+
<html>
|
|
1120
|
+
<head>
|
|
1121
|
+
<title>Test Article Title</title>
|
|
1122
|
+
<meta name="author" content="John Doe">
|
|
1123
|
+
<script>console.log('analytics')</script>
|
|
1124
|
+
<style>.nav { background: blue; }</style>
|
|
1125
|
+
</head>
|
|
1126
|
+
<body>
|
|
1127
|
+
<nav class="nav">
|
|
1128
|
+
<a href="/">Home</a>
|
|
1129
|
+
<a href="/about">About</a>
|
|
1130
|
+
</nav>
|
|
1131
|
+
<article>
|
|
1132
|
+
<h1>Test Article Title</h1>
|
|
1133
|
+
<p>This is the first paragraph of the article content.</p>
|
|
1134
|
+
<p>This is the second paragraph with more details about the topic.</p>
|
|
1135
|
+
<p>The article continues with important information here.</p>
|
|
1136
|
+
</article>
|
|
1137
|
+
<aside>
|
|
1138
|
+
<h3>Related Posts</h3>
|
|
1139
|
+
<ul><li>Post 1</li><li>Post 2</li></ul>
|
|
1140
|
+
</aside>
|
|
1141
|
+
<footer>Copyright 2024</footer>
|
|
1142
|
+
</body>
|
|
1143
|
+
</html>
|
|
1144
|
+
`);
|
|
1145
|
+
await page.bringToFront();
|
|
1146
|
+
await serviceWorker.evaluate(async () => {
|
|
1147
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
1148
|
+
});
|
|
1149
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
1150
|
+
// Test basic getPageMarkdown
|
|
1151
|
+
const result = await client.callTool({
|
|
1152
|
+
name: 'execute',
|
|
1153
|
+
arguments: {
|
|
1154
|
+
code: js `
|
|
1155
|
+
let testPage;
|
|
1156
|
+
for (const p of context.pages()) {
|
|
1157
|
+
const html = await p.content();
|
|
1158
|
+
if (html.includes('Test Article Title')) { testPage = p; break; }
|
|
1159
|
+
}
|
|
1160
|
+
if (!testPage) throw new Error('Test page not found');
|
|
1161
|
+
const content = await getPageMarkdown({ page: testPage });
|
|
1162
|
+
console.log(content);
|
|
1163
|
+
`,
|
|
1164
|
+
timeout: 15000,
|
|
1165
|
+
},
|
|
1166
|
+
});
|
|
1167
|
+
expect(result.isError).toBeFalsy();
|
|
1168
|
+
const text = result.content[0]?.text || '';
|
|
1169
|
+
// Snapshot the full output
|
|
1170
|
+
await expect(text).toMatchFileSnapshot('./snapshots/page-markdown-output.txt');
|
|
1171
|
+
// Should contain article content
|
|
1172
|
+
expect(text).toContain('Test Article Title');
|
|
1173
|
+
expect(text).toContain('first paragraph');
|
|
1174
|
+
expect(text).toContain('second paragraph');
|
|
1175
|
+
// Should NOT contain script/style content
|
|
1176
|
+
expect(text).not.toContain('analytics');
|
|
1177
|
+
expect(text).not.toContain('background: blue');
|
|
1178
|
+
// Test search functionality
|
|
1179
|
+
const searchResult = await client.callTool({
|
|
1180
|
+
name: 'execute',
|
|
1181
|
+
arguments: {
|
|
1182
|
+
code: js `
|
|
1183
|
+
let testPage;
|
|
1184
|
+
for (const p of context.pages()) {
|
|
1185
|
+
const html = await p.content();
|
|
1186
|
+
if (html.includes('Test Article Title')) { testPage = p; break; }
|
|
1187
|
+
}
|
|
1188
|
+
if (!testPage) throw new Error('Test page not found');
|
|
1189
|
+
const content = await getPageMarkdown({ page: testPage, search: /important/i, showDiffSinceLastCall: false });
|
|
1190
|
+
return content;
|
|
1191
|
+
`,
|
|
1192
|
+
timeout: 15000,
|
|
1193
|
+
},
|
|
1194
|
+
});
|
|
1195
|
+
expect(searchResult.isError).toBeFalsy();
|
|
1196
|
+
const searchText = searchResult.content[0]?.text || '';
|
|
1197
|
+
expect(searchText).toContain('important');
|
|
1198
|
+
await page.close();
|
|
1199
|
+
}, 60000);
|
|
1200
|
+
it('should handle default page being closed and switch to another available page', async () => {
|
|
1201
|
+
// This test verifies that when the default `page` in MCP scope is closed,
|
|
1202
|
+
// the MCP automatically switches to another available page instead of failing
|
|
1203
|
+
// with cryptic "page closed" errors.
|
|
1204
|
+
const browserContext = getBrowserContext();
|
|
1205
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1206
|
+
// 1. Disconnect everything to start fresh
|
|
1207
|
+
await serviceWorker.evaluate(async () => {
|
|
1208
|
+
await globalThis.disconnectEverything();
|
|
1209
|
+
});
|
|
1210
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1211
|
+
// 2. Create first page and enable extension
|
|
1212
|
+
const page1 = await browserContext.newPage();
|
|
1213
|
+
await page1.goto('https://example.com/first-page');
|
|
1214
|
+
await page1.bringToFront();
|
|
1215
|
+
await serviceWorker.evaluate(async () => {
|
|
1216
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
1217
|
+
});
|
|
1218
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1219
|
+
// 3. Reset MCP to ensure page1 becomes the default page (only page available)
|
|
1220
|
+
const resetResult = await client.callTool({
|
|
1221
|
+
name: 'reset',
|
|
1222
|
+
arguments: {},
|
|
1223
|
+
});
|
|
1224
|
+
expect(resetResult.content[0].text).toContain('Connection reset successfully');
|
|
1225
|
+
// 4. Verify initial page is accessible via default `page`
|
|
1226
|
+
const initialResult = await client.callTool({
|
|
1227
|
+
name: 'execute',
|
|
1228
|
+
arguments: {
|
|
1229
|
+
code: js `
|
|
1230
|
+
const url = page.url();
|
|
1231
|
+
console.log('Initial page URL:', url);
|
|
1232
|
+
return { url };
|
|
1233
|
+
`,
|
|
1234
|
+
},
|
|
1235
|
+
});
|
|
1236
|
+
expect(initialResult.content[0].text).toContain('first-page');
|
|
1237
|
+
// 5. Create second page and enable extension
|
|
1238
|
+
const page2 = await browserContext.newPage();
|
|
1239
|
+
await page2.goto('https://example.com/second-page');
|
|
1240
|
+
await page2.bringToFront();
|
|
1241
|
+
await serviceWorker.evaluate(async () => {
|
|
1242
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
1243
|
+
});
|
|
1244
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1245
|
+
// 6. Close the first page (which is the default `page` in MCP scope)
|
|
1246
|
+
await page1.close();
|
|
1247
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1248
|
+
// 7. Execute code via MCP - should NOT fail with "page closed" error
|
|
1249
|
+
// Instead, it should automatically switch to the second page
|
|
1250
|
+
const afterCloseResult = await client.callTool({
|
|
1251
|
+
name: 'execute',
|
|
1252
|
+
arguments: {
|
|
1253
|
+
code: js `
|
|
1254
|
+
const url = page.url();
|
|
1255
|
+
console.log('Page URL after close:', url);
|
|
1256
|
+
const title = await page.title();
|
|
1257
|
+
return { url, title };
|
|
1258
|
+
`,
|
|
1259
|
+
},
|
|
1260
|
+
});
|
|
1261
|
+
// Should succeed and return the second page's info
|
|
1262
|
+
expect(afterCloseResult.isError).toBeFalsy();
|
|
1263
|
+
const output = afterCloseResult.content[0].text;
|
|
1264
|
+
expect(output).toContain('second-page');
|
|
1265
|
+
expect(output).not.toContain('page closed');
|
|
1266
|
+
expect(output).not.toContain('Target closed');
|
|
1267
|
+
// Cleanup
|
|
1268
|
+
await page2.close();
|
|
1269
|
+
}, 60000);
|
|
1270
|
+
it('should show descriptive error when clicking a hidden element', async () => {
|
|
1271
|
+
await ensureConnectedTabForExecute();
|
|
1272
|
+
// Create a fresh page and set content with a collapsed details element
|
|
1273
|
+
await client.callTool({
|
|
1274
|
+
name: 'execute',
|
|
1275
|
+
arguments: {
|
|
1276
|
+
code: js `
|
|
1277
|
+
state.errorTestPage = await context.newPage();
|
|
1278
|
+
await state.errorTestPage.setContent(\`
|
|
1279
|
+
<details>
|
|
1280
|
+
<summary>Toggle</summary>
|
|
1281
|
+
<button id="hidden-btn">Hidden Button</button>
|
|
1282
|
+
</details>
|
|
1283
|
+
\`);
|
|
1284
|
+
`,
|
|
1285
|
+
},
|
|
1286
|
+
});
|
|
1287
|
+
const result = await client.callTool({
|
|
1288
|
+
name: 'execute',
|
|
1289
|
+
arguments: {
|
|
1290
|
+
code: js `
|
|
1291
|
+
await state.errorTestPage.click('#hidden-btn', { timeout: 100 });
|
|
1292
|
+
`,
|
|
1293
|
+
},
|
|
1294
|
+
});
|
|
1295
|
+
expect(result).toMatchInlineSnapshot(`
|
|
1296
|
+
{
|
|
1297
|
+
"content": [
|
|
1298
|
+
{
|
|
1299
|
+
"text": "
|
|
1300
|
+
Error executing code: page.click: Timeout 100ms exceeded. Element is not visible — it may be hidden by CSS, inside a collapsed <details>, inactive tab, or closed accordion. Try: interact with the page to reveal it first, or use { force: true } to skip visibility checks
|
|
1301
|
+
Call log:
|
|
1302
|
+
[2m - waiting for locator('#hidden-btn')[22m
|
|
1303
|
+
[2m - locator resolved to <button id="hidden-btn">Hidden Button</button>[22m
|
|
1304
|
+
[2m - attempting click action[22m
|
|
1305
|
+
[2m 2 × waiting for element to be visible, enabled and stable[22m
|
|
1306
|
+
[2m - element is not visible[22m
|
|
1307
|
+
[2m - retrying click action[22m
|
|
1308
|
+
[2m - waiting 20ms[22m
|
|
1309
|
+
[2m - waiting for element to be visible, enabled and stable[22m
|
|
1310
|
+
[2m - element is not visible[22m
|
|
1311
|
+
[2m - retrying click action[22m
|
|
1312
|
+
[2m - waiting 100ms[22m
|
|
1313
|
+
",
|
|
1314
|
+
"type": "text",
|
|
1315
|
+
},
|
|
1316
|
+
],
|
|
1317
|
+
"isError": true,
|
|
1318
|
+
}
|
|
1319
|
+
`);
|
|
1320
|
+
// Cleanup
|
|
1321
|
+
await client.callTool({ name: 'execute', arguments: { code: js `await state.errorTestPage.close(); delete state.errorTestPage;` } });
|
|
1322
|
+
}, 30000);
|
|
1323
|
+
it('should show descriptive error when clicking an element covered by another', async () => {
|
|
1324
|
+
await ensureConnectedTabForExecute();
|
|
1325
|
+
await client.callTool({
|
|
1326
|
+
name: 'execute',
|
|
1327
|
+
arguments: {
|
|
1328
|
+
code: js `
|
|
1329
|
+
state.errorTestPage = await context.newPage();
|
|
1330
|
+
await state.errorTestPage.setContent(\`
|
|
1331
|
+
<div style="position:relative">
|
|
1332
|
+
<button id="covered-btn" style="position:absolute;top:0;left:0">Covered</button>
|
|
1333
|
+
<div id="overlay" style="position:absolute;top:0;left:0;width:200px;height:200px;background:red;z-index:10">Overlay</div>
|
|
1334
|
+
</div>
|
|
1335
|
+
\`);
|
|
1336
|
+
`,
|
|
1337
|
+
},
|
|
1338
|
+
});
|
|
1339
|
+
const result = await client.callTool({
|
|
1340
|
+
name: 'execute',
|
|
1341
|
+
arguments: {
|
|
1342
|
+
code: js `
|
|
1343
|
+
await state.errorTestPage.click('#covered-btn', { timeout: 100 });
|
|
1344
|
+
`,
|
|
1345
|
+
},
|
|
1346
|
+
});
|
|
1347
|
+
expect(result).toMatchInlineSnapshot(`
|
|
1348
|
+
{
|
|
1349
|
+
"content": [
|
|
1350
|
+
{
|
|
1351
|
+
"text": "
|
|
1352
|
+
Error executing code: page.click: Timeout 100ms exceeded. <div id="overlay">Overlay</div> intercepts pointer events
|
|
1353
|
+
Call log:
|
|
1354
|
+
[2m - waiting for locator('#covered-btn')[22m
|
|
1355
|
+
[2m - locator resolved to <button id="covered-btn">Covered</button>[22m
|
|
1356
|
+
[2m - attempting click action[22m
|
|
1357
|
+
[2m 2 × waiting for element to be visible, enabled and stable[22m
|
|
1358
|
+
[2m - element is visible, enabled and stable[22m
|
|
1359
|
+
[2m - scrolling into view if needed[22m
|
|
1360
|
+
[2m - done scrolling[22m
|
|
1361
|
+
[2m - <div id="overlay">Overlay</div> intercepts pointer events[22m
|
|
1362
|
+
[2m - retrying click action[22m
|
|
1363
|
+
[2m - waiting 20ms[22m
|
|
1364
|
+
[2m - waiting for element to be visible, enabled and stable[22m
|
|
1365
|
+
[2m - element is visible, enabled and stable[22m
|
|
1366
|
+
[2m - scrolling into view if needed[22m
|
|
1367
|
+
[2m - done scrolling[22m
|
|
1368
|
+
[2m - <div id="overlay">Overlay</div> intercepts pointer events[22m
|
|
1369
|
+
[2m - retrying click action[22m
|
|
1370
|
+
[2m - waiting 100ms[22m
|
|
1371
|
+
",
|
|
1372
|
+
"type": "text",
|
|
1373
|
+
},
|
|
1374
|
+
],
|
|
1375
|
+
"isError": true,
|
|
1376
|
+
}
|
|
1377
|
+
`);
|
|
1378
|
+
await client.callTool({ name: 'execute', arguments: { code: js `await state.errorTestPage.close(); delete state.errorTestPage;` } });
|
|
1379
|
+
}, 30000);
|
|
1380
|
+
it('should show descriptive error when clicking a display:none element', async () => {
|
|
1381
|
+
await ensureConnectedTabForExecute();
|
|
1382
|
+
await client.callTool({
|
|
1383
|
+
name: 'execute',
|
|
1384
|
+
arguments: {
|
|
1385
|
+
code: js `
|
|
1386
|
+
state.errorTestPage = await context.newPage();
|
|
1387
|
+
await state.errorTestPage.setContent('<button id="invisible" style="display:none">Invisible</button>');
|
|
1388
|
+
`,
|
|
1389
|
+
},
|
|
1390
|
+
});
|
|
1391
|
+
const result = await client.callTool({
|
|
1392
|
+
name: 'execute',
|
|
1393
|
+
arguments: {
|
|
1394
|
+
code: js `
|
|
1395
|
+
await state.errorTestPage.click('#invisible', { timeout: 100 });
|
|
1396
|
+
`,
|
|
1397
|
+
},
|
|
1398
|
+
});
|
|
1399
|
+
expect(result).toMatchInlineSnapshot(`
|
|
1400
|
+
{
|
|
1401
|
+
"content": [
|
|
1402
|
+
{
|
|
1403
|
+
"text": "
|
|
1404
|
+
Error executing code: page.click: Timeout 100ms exceeded. Element is not visible — it may be hidden by CSS, inside a collapsed <details>, inactive tab, or closed accordion. Try: interact with the page to reveal it first, or use { force: true } to skip visibility checks
|
|
1405
|
+
Call log:
|
|
1406
|
+
[2m - waiting for locator('#invisible')[22m
|
|
1407
|
+
[2m - locator resolved to <button id="invisible">Invisible</button>[22m
|
|
1408
|
+
[2m - attempting click action[22m
|
|
1409
|
+
[2m 2 × waiting for element to be visible, enabled and stable[22m
|
|
1410
|
+
[2m - element is not visible[22m
|
|
1411
|
+
[2m - retrying click action[22m
|
|
1412
|
+
[2m - waiting 20ms[22m
|
|
1413
|
+
[2m - waiting for element to be visible, enabled and stable[22m
|
|
1414
|
+
[2m - element is not visible[22m
|
|
1415
|
+
[2m - retrying click action[22m
|
|
1416
|
+
[2m - waiting 100ms[22m
|
|
1417
|
+
",
|
|
1418
|
+
"type": "text",
|
|
1419
|
+
},
|
|
1420
|
+
],
|
|
1421
|
+
"isError": true,
|
|
1422
|
+
}
|
|
1423
|
+
`);
|
|
1424
|
+
await client.callTool({ name: 'execute', arguments: { code: js `await state.errorTestPage.close(); delete state.errorTestPage;` } });
|
|
1425
|
+
}, 30000);
|
|
1426
|
+
});
|
|
1427
|
+
//# sourceMappingURL=relay-core.test.js.map
|