@roxybrowser/playwright 2.0.2-beta.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/README.md +115 -0
- package/dist/apiRequestContext.d.ts +52 -0
- package/dist/apiRequestContext.d.ts.map +1 -0
- package/dist/apiRequestContext.js +632 -0
- package/dist/apiRequestContext.js.map +1 -0
- package/dist/ariaSnapshot.d.ts +38 -0
- package/dist/ariaSnapshot.d.ts.map +1 -0
- package/dist/ariaSnapshot.js +1137 -0
- package/dist/ariaSnapshot.js.map +1 -0
- package/dist/assertions.d.ts +2 -0
- package/dist/assertions.d.ts.map +1 -0
- package/dist/assertions.js +6 -0
- package/dist/assertions.js.map +1 -0
- package/dist/bin/roxybrowser-mcp.d.ts +3 -0
- package/dist/bin/roxybrowser-mcp.d.ts.map +1 -0
- package/dist/bin/roxybrowser-mcp.js +7 -0
- package/dist/bin/roxybrowser-mcp.js.map +1 -0
- package/dist/browser.d.ts +15 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +67 -0
- package/dist/browser.js.map +1 -0
- package/dist/browserContext.d.ts +156 -0
- package/dist/browserContext.d.ts.map +1 -0
- package/dist/browserContext.js +1129 -0
- package/dist/browserContext.js.map +1 -0
- package/dist/browserContextClock.d.ts +54 -0
- package/dist/browserContextClock.d.ts.map +1 -0
- package/dist/browserContextClock.js +147 -0
- package/dist/browserContextClock.js.map +1 -0
- package/dist/browserType.d.ts +16 -0
- package/dist/browserType.d.ts.map +1 -0
- package/dist/browserType.js +68 -0
- package/dist/browserType.js.map +1 -0
- package/dist/bundle.d.ts +20 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +20 -0
- package/dist/bundle.js.map +1 -0
- package/dist/clock.d.ts +43 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +70 -0
- package/dist/clock.js.map +1 -0
- package/dist/elementHandle.d.ts +100 -0
- package/dist/elementHandle.d.ts.map +1 -0
- package/dist/elementHandle.js +423 -0
- package/dist/elementHandle.js.map +1 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +19 -0
- package/dist/errors.js.map +1 -0
- package/dist/evaluation.d.ts +4 -0
- package/dist/evaluation.d.ts.map +1 -0
- package/dist/evaluation.js +9 -0
- package/dist/evaluation.js.map +1 -0
- package/dist/frame.d.ts +139 -0
- package/dist/frame.d.ts.map +1 -0
- package/dist/frame.js +451 -0
- package/dist/frame.js.map +1 -0
- package/dist/httpHeaders.d.ts +6 -0
- package/dist/httpHeaders.d.ts.map +1 -0
- package/dist/httpHeaders.js +23 -0
- package/dist/httpHeaders.js.map +1 -0
- package/dist/human/controller.d.ts +15 -0
- package/dist/human/controller.d.ts.map +1 -0
- package/dist/human/controller.js +67 -0
- package/dist/human/controller.js.map +1 -0
- package/dist/human/profile.d.ts +5 -0
- package/dist/human/profile.d.ts.map +1 -0
- package/dist/human/profile.js +50 -0
- package/dist/human/profile.js.map +1 -0
- package/dist/human/types.d.ts +28 -0
- package/dist/human/types.d.ts.map +1 -0
- package/dist/human/types.js +2 -0
- package/dist/human/types.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/inputFiles.d.ts +18 -0
- package/dist/inputFiles.d.ts.map +1 -0
- package/dist/inputFiles.js +192 -0
- package/dist/inputFiles.js.map +1 -0
- package/dist/jsHandle.d.ts +32 -0
- package/dist/jsHandle.d.ts.map +1 -0
- package/dist/jsHandle.js +148 -0
- package/dist/jsHandle.js.map +1 -0
- package/dist/locator.d.ts +190 -0
- package/dist/locator.d.ts.map +1 -0
- package/dist/locator.js +883 -0
- package/dist/locator.js.map +1 -0
- package/dist/locatorSelectors.d.ts +11 -0
- package/dist/locatorSelectors.d.ts.map +1 -0
- package/dist/locatorSelectors.js +50 -0
- package/dist/locatorSelectors.js.map +1 -0
- package/dist/mcp/backend/common.d.ts +4 -0
- package/dist/mcp/backend/common.d.ts.map +1 -0
- package/dist/mcp/backend/common.js +40 -0
- package/dist/mcp/backend/common.js.map +1 -0
- package/dist/mcp/backend/connect.d.ts +10 -0
- package/dist/mcp/backend/connect.d.ts.map +1 -0
- package/dist/mcp/backend/connect.js +30 -0
- package/dist/mcp/backend/connect.js.map +1 -0
- package/dist/mcp/backend/console.d.ts +13 -0
- package/dist/mcp/backend/console.d.ts.map +1 -0
- package/dist/mcp/backend/console.js +36 -0
- package/dist/mcp/backend/console.js.map +1 -0
- package/dist/mcp/backend/context.d.ts +27 -0
- package/dist/mcp/backend/context.d.ts.map +1 -0
- package/dist/mcp/backend/context.js +33 -0
- package/dist/mcp/backend/context.js.map +1 -0
- package/dist/mcp/backend/dialogs.d.ts +7 -0
- package/dist/mcp/backend/dialogs.d.ts.map +1 -0
- package/dist/mcp/backend/dialogs.js +24 -0
- package/dist/mcp/backend/dialogs.js.map +1 -0
- package/dist/mcp/backend/evaluate.d.ts +9 -0
- package/dist/mcp/backend/evaluate.d.ts.map +1 -0
- package/dist/mcp/backend/evaluate.js +31 -0
- package/dist/mcp/backend/evaluate.js.map +1 -0
- package/dist/mcp/backend/files.d.ts +9 -0
- package/dist/mcp/backend/files.d.ts.map +1 -0
- package/dist/mcp/backend/files.js +39 -0
- package/dist/mcp/backend/files.js.map +1 -0
- package/dist/mcp/backend/keyboard.d.ts +22 -0
- package/dist/mcp/backend/keyboard.d.ts.map +1 -0
- package/dist/mcp/backend/keyboard.js +67 -0
- package/dist/mcp/backend/keyboard.js.map +1 -0
- package/dist/mcp/backend/navigate.d.ts +4 -0
- package/dist/mcp/backend/navigate.d.ts.map +1 -0
- package/dist/mcp/backend/navigate.js +82 -0
- package/dist/mcp/backend/navigate.js.map +1 -0
- package/dist/mcp/backend/network.d.ts +17 -0
- package/dist/mcp/backend/network.d.ts.map +1 -0
- package/dist/mcp/backend/network.js +144 -0
- package/dist/mcp/backend/network.js.map +1 -0
- package/dist/mcp/backend/response.d.ts +26 -0
- package/dist/mcp/backend/response.d.ts.map +1 -0
- package/dist/mcp/backend/response.js +109 -0
- package/dist/mcp/backend/response.js.map +1 -0
- package/dist/mcp/backend/runCode.d.ts +6 -0
- package/dist/mcp/backend/runCode.d.ts.map +1 -0
- package/dist/mcp/backend/runCode.js +20 -0
- package/dist/mcp/backend/runCode.js.map +1 -0
- package/dist/mcp/backend/screenshot.d.ts +13 -0
- package/dist/mcp/backend/screenshot.d.ts.map +1 -0
- package/dist/mcp/backend/screenshot.js +37 -0
- package/dist/mcp/backend/screenshot.js.map +1 -0
- package/dist/mcp/backend/snapshot.d.ts +70 -0
- package/dist/mcp/backend/snapshot.d.ts.map +1 -0
- package/dist/mcp/backend/snapshot.js +185 -0
- package/dist/mcp/backend/snapshot.js.map +1 -0
- package/dist/mcp/backend/tab.d.ts +72 -0
- package/dist/mcp/backend/tab.d.ts.map +1 -0
- package/dist/mcp/backend/tab.js +99 -0
- package/dist/mcp/backend/tab.js.map +1 -0
- package/dist/mcp/backend/tabs.d.ts +13 -0
- package/dist/mcp/backend/tabs.d.ts.map +1 -0
- package/dist/mcp/backend/tabs.js +44 -0
- package/dist/mcp/backend/tabs.js.map +1 -0
- package/dist/mcp/backend/tool.d.ts +52 -0
- package/dist/mcp/backend/tool.d.ts.map +1 -0
- package/dist/mcp/backend/tool.js +28 -0
- package/dist/mcp/backend/tool.js.map +1 -0
- package/dist/mcp/backend/tools.d.ts +4 -0
- package/dist/mcp/backend/tools.d.ts.map +1 -0
- package/dist/mcp/backend/tools.js +30 -0
- package/dist/mcp/backend/tools.js.map +1 -0
- package/dist/mcp/connectedBrowser.d.ts +3 -0
- package/dist/mcp/connectedBrowser.d.ts.map +1 -0
- package/dist/mcp/connectedBrowser.js +2153 -0
- package/dist/mcp/connectedBrowser.js.map +1 -0
- package/dist/mcp/errors.d.ts +6 -0
- package/dist/mcp/errors.d.ts.map +1 -0
- package/dist/mcp/errors.js +12 -0
- package/dist/mcp/errors.js.map +1 -0
- package/dist/mcp/format.d.ts +12 -0
- package/dist/mcp/format.d.ts.map +1 -0
- package/dist/mcp/format.js +48 -0
- package/dist/mcp/format.js.map +1 -0
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +5 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/output.d.ts +7 -0
- package/dist/mcp/output.d.ts.map +1 -0
- package/dist/mcp/output.js +39 -0
- package/dist/mcp/output.js.map +1 -0
- package/dist/mcp/runtime.d.ts +122 -0
- package/dist/mcp/runtime.d.ts.map +1 -0
- package/dist/mcp/runtime.js +524 -0
- package/dist/mcp/runtime.js.map +1 -0
- package/dist/mcp/schemas.d.ts +61 -0
- package/dist/mcp/schemas.d.ts.map +1 -0
- package/dist/mcp/schemas.js +41 -0
- package/dist/mcp/schemas.js.map +1 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +117 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/snapshot.d.ts +3 -0
- package/dist/mcp/snapshot.d.ts.map +1 -0
- package/dist/mcp/snapshot.js +3 -0
- package/dist/mcp/snapshot.js.map +1 -0
- package/dist/mcp/tool.d.ts +19 -0
- package/dist/mcp/tool.d.ts.map +1 -0
- package/dist/mcp/tool.js +10 -0
- package/dist/mcp/tool.js.map +1 -0
- package/dist/mcp/tools/common.d.ts +6 -0
- package/dist/mcp/tools/common.d.ts.map +1 -0
- package/dist/mcp/tools/common.js +34 -0
- package/dist/mcp/tools/common.js.map +1 -0
- package/dist/mcp/tools/connect.d.ts +6 -0
- package/dist/mcp/tools/connect.d.ts.map +1 -0
- package/dist/mcp/tools/connect.js +28 -0
- package/dist/mcp/tools/connect.js.map +1 -0
- package/dist/mcp/tools/console.d.ts +6 -0
- package/dist/mcp/tools/console.d.ts.map +1 -0
- package/dist/mcp/tools/console.js +36 -0
- package/dist/mcp/tools/console.js.map +1 -0
- package/dist/mcp/tools/dialog.d.ts +6 -0
- package/dist/mcp/tools/dialog.d.ts.map +1 -0
- package/dist/mcp/tools/dialog.js +22 -0
- package/dist/mcp/tools/dialog.js.map +1 -0
- package/dist/mcp/tools/evaluate.d.ts +6 -0
- package/dist/mcp/tools/evaluate.d.ts.map +1 -0
- package/dist/mcp/tools/evaluate.js +31 -0
- package/dist/mcp/tools/evaluate.js.map +1 -0
- package/dist/mcp/tools/form.d.ts +6 -0
- package/dist/mcp/tools/form.d.ts.map +1 -0
- package/dist/mcp/tools/form.js +129 -0
- package/dist/mcp/tools/form.js.map +1 -0
- package/dist/mcp/tools/index.d.ts +3 -0
- package/dist/mcp/tools/index.d.ts.map +1 -0
- package/dist/mcp/tools/index.js +7 -0
- package/dist/mcp/tools/index.js.map +1 -0
- package/dist/mcp/tools/keyboard.d.ts +3 -0
- package/dist/mcp/tools/keyboard.d.ts.map +1 -0
- package/dist/mcp/tools/keyboard.js +44 -0
- package/dist/mcp/tools/keyboard.js.map +1 -0
- package/dist/mcp/tools/mouse.d.ts +3 -0
- package/dist/mcp/tools/mouse.d.ts.map +1 -0
- package/dist/mcp/tools/mouse.js +89 -0
- package/dist/mcp/tools/mouse.js.map +1 -0
- package/dist/mcp/tools/navigate.d.ts +6 -0
- package/dist/mcp/tools/navigate.d.ts.map +1 -0
- package/dist/mcp/tools/navigate.js +74 -0
- package/dist/mcp/tools/navigate.js.map +1 -0
- package/dist/mcp/tools/network.d.ts +6 -0
- package/dist/mcp/tools/network.d.ts.map +1 -0
- package/dist/mcp/tools/network.js +142 -0
- package/dist/mcp/tools/network.js.map +1 -0
- package/dist/mcp/tools/runCode.d.ts +6 -0
- package/dist/mcp/tools/runCode.d.ts.map +1 -0
- package/dist/mcp/tools/runCode.js +24 -0
- package/dist/mcp/tools/runCode.js.map +1 -0
- package/dist/mcp/tools/screenshot.d.ts +6 -0
- package/dist/mcp/tools/screenshot.d.ts.map +1 -0
- package/dist/mcp/tools/screenshot.js +46 -0
- package/dist/mcp/tools/screenshot.js.map +1 -0
- package/dist/mcp/tools/snapshot.d.ts +6 -0
- package/dist/mcp/tools/snapshot.d.ts.map +1 -0
- package/dist/mcp/tools/snapshot.js +56 -0
- package/dist/mcp/tools/snapshot.js.map +1 -0
- package/dist/mcp/tools/tabs.d.ts +6 -0
- package/dist/mcp/tools/tabs.d.ts.map +1 -0
- package/dist/mcp/tools/tabs.js +34 -0
- package/dist/mcp/tools/tabs.js.map +1 -0
- package/dist/mcp/transports/http.d.ts +3 -0
- package/dist/mcp/transports/http.d.ts.map +1 -0
- package/dist/mcp/transports/http.js +134 -0
- package/dist/mcp/transports/http.js.map +1 -0
- package/dist/mcp/transports/inMemory.d.ts +3 -0
- package/dist/mcp/transports/inMemory.d.ts.map +1 -0
- package/dist/mcp/transports/inMemory.js +18 -0
- package/dist/mcp/transports/inMemory.js.map +1 -0
- package/dist/mcp/transports/stdio.d.ts +3 -0
- package/dist/mcp/transports/stdio.d.ts.map +1 -0
- package/dist/mcp/transports/stdio.js +16 -0
- package/dist/mcp/transports/stdio.js.map +1 -0
- package/dist/mcp/types.d.ts +193 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/navigationResult.d.ts +10 -0
- package/dist/navigationResult.d.ts.map +1 -0
- package/dist/navigationResult.js +15 -0
- package/dist/navigationResult.js.map +1 -0
- package/dist/page.d.ts +907 -0
- package/dist/page.d.ts.map +1 -0
- package/dist/page.js +6385 -0
- package/dist/page.js.map +1 -0
- package/dist/pageApiReport.d.ts +31 -0
- package/dist/pageApiReport.d.ts.map +1 -0
- package/dist/pageApiReport.js +218 -0
- package/dist/pageApiReport.js.map +1 -0
- package/dist/pageResponse.d.ts +18 -0
- package/dist/pageResponse.d.ts.map +1 -0
- package/dist/pageResponse.js +23 -0
- package/dist/pageResponse.js.map +1 -0
- package/dist/processCleanup.d.ts +8 -0
- package/dist/processCleanup.d.ts.map +1 -0
- package/dist/processCleanup.js +413 -0
- package/dist/processCleanup.js.map +1 -0
- package/dist/protocol/adapter.d.ts +406 -0
- package/dist/protocol/adapter.d.ts.map +1 -0
- package/dist/protocol/adapter.js +8 -0
- package/dist/protocol/adapter.js.map +1 -0
- package/dist/protocol/bidi/backend.d.ts +8 -0
- package/dist/protocol/bidi/backend.d.ts.map +1 -0
- package/dist/protocol/bidi/backend.js +3616 -0
- package/dist/protocol/bidi/backend.js.map +1 -0
- package/dist/protocol/bidi/client.d.ts +104 -0
- package/dist/protocol/bidi/client.d.ts.map +1 -0
- package/dist/protocol/bidi/client.js +228 -0
- package/dist/protocol/bidi/client.js.map +1 -0
- package/dist/protocol/capabilities.d.ts +11 -0
- package/dist/protocol/capabilities.d.ts.map +1 -0
- package/dist/protocol/capabilities.js +2 -0
- package/dist/protocol/capabilities.js.map +1 -0
- package/dist/protocol/cdp/backend.d.ts +24 -0
- package/dist/protocol/cdp/backend.d.ts.map +1 -0
- package/dist/protocol/cdp/backend.js +8428 -0
- package/dist/protocol/cdp/backend.js.map +1 -0
- package/dist/protocol/evaluate.d.ts +2 -0
- package/dist/protocol/evaluate.d.ts.map +1 -0
- package/dist/protocol/evaluate.js +14 -0
- package/dist/protocol/evaluate.js.map +1 -0
- package/dist/protocol/evaluationSerializer.d.ts +6 -0
- package/dist/protocol/evaluationSerializer.d.ts.map +1 -0
- package/dist/protocol/evaluationSerializer.js +234 -0
- package/dist/protocol/evaluationSerializer.js.map +1 -0
- package/dist/protocol/keyboardInput.d.ts +34 -0
- package/dist/protocol/keyboardInput.d.ts.map +1 -0
- package/dist/protocol/keyboardInput.js +246 -0
- package/dist/protocol/keyboardInput.js.map +1 -0
- package/dist/protocol/routing.d.ts +40 -0
- package/dist/protocol/routing.d.ts.map +1 -0
- package/dist/protocol/routing.js +2 -0
- package/dist/protocol/routing.js.map +1 -0
- package/dist/protocol/selectorRuntime.d.ts +30 -0
- package/dist/protocol/selectorRuntime.d.ts.map +1 -0
- package/dist/protocol/selectorRuntime.js +1814 -0
- package/dist/protocol/selectorRuntime.js.map +1 -0
- package/dist/protocol/webdriver-classic/backend.d.ts +6 -0
- package/dist/protocol/webdriver-classic/backend.d.ts.map +1 -0
- package/dist/protocol/webdriver-classic/backend.js +158 -0
- package/dist/protocol/webdriver-classic/backend.js.map +1 -0
- package/dist/routeHandler.d.ts +15 -0
- package/dist/routeHandler.d.ts.map +1 -0
- package/dist/routeHandler.js +2 -0
- package/dist/routeHandler.js.map +1 -0
- package/dist/roxybrowser.bundle.js +78236 -0
- package/dist/roxybrowser.bundle.js.map +1 -0
- package/dist/screencast.d.ts +57 -0
- package/dist/screencast.d.ts.map +1 -0
- package/dist/screencast.js +155 -0
- package/dist/screencast.js.map +1 -0
- package/dist/screencastActions.d.ts +2 -0
- package/dist/screencastActions.d.ts.map +1 -0
- package/dist/screencastActions.js +183 -0
- package/dist/screencastActions.js.map +1 -0
- package/dist/screencastOverlay.d.ts +3 -0
- package/dist/screencastOverlay.d.ts.map +1 -0
- package/dist/screencastOverlay.js +119 -0
- package/dist/screencastOverlay.js.map +1 -0
- package/dist/screenshotOptions.d.ts +26 -0
- package/dist/screenshotOptions.d.ts.map +1 -0
- package/dist/screenshotOptions.js +171 -0
- package/dist/screenshotOptions.js.map +1 -0
- package/dist/screenshotPreparation.d.ts +21 -0
- package/dist/screenshotPreparation.d.ts.map +1 -0
- package/dist/screenshotPreparation.js +199 -0
- package/dist/screenshotPreparation.js.map +1 -0
- package/dist/selectOptionValues.d.ts +10 -0
- package/dist/selectOptionValues.d.ts.map +1 -0
- package/dist/selectOptionValues.js +60 -0
- package/dist/selectOptionValues.js.map +1 -0
- package/dist/selectors.d.ts +3 -0
- package/dist/selectors.d.ts.map +1 -0
- package/dist/selectors.js +287 -0
- package/dist/selectors.js.map +1 -0
- package/dist/types/api.d.ts +2164 -0
- package/dist/types/api.d.ts.map +1 -0
- package/dist/types/api.js +2 -0
- package/dist/types/api.js.map +1 -0
- package/dist/types/events.d.ts +158 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +2 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/options.d.ts +263 -0
- package/dist/types/options.d.ts.map +1 -0
- package/dist/types/options.js +2 -0
- package/dist/types/options.js.map +1 -0
- package/dist/urlMatch.d.ts +19 -0
- package/dist/urlMatch.d.ts.map +1 -0
- package/dist/urlMatch.js +183 -0
- package/dist/urlMatch.js.map +1 -0
- package/dist/utilityScriptSerializers.d.ts +61 -0
- package/dist/utilityScriptSerializers.d.ts.map +1 -0
- package/dist/utilityScriptSerializers.js +297 -0
- package/dist/utilityScriptSerializers.js.map +1 -0
- package/dist/vendor/playwright/ariaSnapshotEvaluate.d.ts +2 -0
- package/dist/vendor/playwright/ariaSnapshotEvaluate.d.ts.map +1 -0
- package/dist/vendor/playwright/ariaSnapshotEvaluate.js +207 -0
- package/dist/vendor/playwright/ariaSnapshotEvaluate.js.map +1 -0
- package/dist/vendor/playwright/generated/clockSource.d.ts +9 -0
- package/dist/vendor/playwright/generated/clockSource.d.ts.map +1 -0
- package/dist/vendor/playwright/generated/clockSource.js +9 -0
- package/dist/vendor/playwright/generated/clockSource.js.map +1 -0
- package/dist/vendor/playwright/generated/injectedScriptSource.d.ts +9 -0
- package/dist/vendor/playwright/generated/injectedScriptSource.d.ts.map +1 -0
- package/dist/vendor/playwright/generated/injectedScriptSource.js +9 -0
- package/dist/vendor/playwright/generated/injectedScriptSource.js.map +1 -0
- package/dist/video.d.ts +39 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +235 -0
- package/dist/video.js.map +1 -0
- package/dist/waitForSelector.d.ts +7 -0
- package/dist/waitForSelector.d.ts.map +1 -0
- package/dist/waitForSelector.js +23 -0
- package/dist/waitForSelector.js.map +1 -0
- package/dist/worker.d.ts +45 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +192 -0
- package/dist/worker.js.map +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,3616 @@
|
|
|
1
|
+
import { ARIA_REF_SELECTOR_EVALUATE_SOURCE, normalizeAriaSnapshotOptions, withOptionalTimeout } from "../../ariaSnapshot.js";
|
|
2
|
+
import { PLAYWRIGHT_ARIA_SNAPSHOT_EVALUATE_SOURCE as ARIA_SNAPSHOT_EVALUATE_SOURCE } from "../../vendor/playwright/ariaSnapshotEvaluate.js";
|
|
3
|
+
import { NotImplementedInProtocolError, TimeoutError } from "../../errors.js";
|
|
4
|
+
import { mergeExtraHTTPHeaders } from "../../httpHeaders.js";
|
|
5
|
+
import { createPageResponse } from "../../pageResponse.js";
|
|
6
|
+
import { parseSerializedEvaluationResult, wrapWithSerializedEvaluationResult } from "../evaluationSerializer.js";
|
|
7
|
+
import { isKeyboardModifier, isUsKeyboardLayoutKey, keyDescriptionForString, resolveSmartModifierString, splitKeyboardShortcut } from "../keyboardInput.js";
|
|
8
|
+
import { SCROLL_INTO_VIEW_IF_NEEDED_SOURCE, SELECTOR_RUNTIME_SOURCE } from "../selectorRuntime.js";
|
|
9
|
+
import { createChapterOverlayHtml, RENDER_SCREencast_OVERLAYS_SOURCE } from "../../screencastOverlay.js";
|
|
10
|
+
import { RENDER_SCREENCAST_ACTIONS_SOURCE } from "../../screencastActions.js";
|
|
11
|
+
import { createAltTextLocatorSelector, createLabelLocatorSelector, createPlaceholderLocatorSelector, createRoleLocatorSelector, createTestIdLocatorSelector, createTextLocatorSelector, createTitleLocatorSelector } from "../../locatorSelectors.js";
|
|
12
|
+
import { locatorSelectorForPick } from "../adapter.js";
|
|
13
|
+
import { looksLikeFunctionExpression } from "../evaluate.js";
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { accessSync, constants as fsConstants } from "node:fs";
|
|
16
|
+
import { access, mkdtemp, rm } from "node:fs/promises";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { registerTestBrowserProcessForCleanup, terminateProcessTree } from "../../processCleanup.js";
|
|
20
|
+
import { getBidiClientFactory } from "./client.js";
|
|
21
|
+
const BIDI_CAPABILITIES = {
|
|
22
|
+
protocol: "bidi",
|
|
23
|
+
supportsMultipleContexts: true,
|
|
24
|
+
supportsIsolatedWorlds: true,
|
|
25
|
+
supportsLocatorChaining: true,
|
|
26
|
+
supportsInputDispatch: true,
|
|
27
|
+
supportsDownloads: false,
|
|
28
|
+
supportsTracing: false
|
|
29
|
+
};
|
|
30
|
+
const CLEANUP_FIREFOX_PROCESS_TIMEOUT_MS = 5_000;
|
|
31
|
+
const HYDRATE_DECLARATIVE_SHADOW_ROOTS_SOURCE = `() => {
|
|
32
|
+
const hydrate = (root) => {
|
|
33
|
+
for (const template of Array.from(root.querySelectorAll('template[shadowrootmode]'))) {
|
|
34
|
+
const mode = template.getAttribute('shadowrootmode');
|
|
35
|
+
if (mode !== 'open' && mode !== 'closed')
|
|
36
|
+
continue;
|
|
37
|
+
const host = template.parentElement;
|
|
38
|
+
if (!host)
|
|
39
|
+
continue;
|
|
40
|
+
const shadowRoot = host.shadowRoot || host.attachShadow({ mode });
|
|
41
|
+
shadowRoot.append(...Array.from(template.content.childNodes));
|
|
42
|
+
template.remove();
|
|
43
|
+
if (mode === 'open')
|
|
44
|
+
hydrate(shadowRoot);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
hydrate(document);
|
|
48
|
+
}`;
|
|
49
|
+
function locatorOperation(payload) {
|
|
50
|
+
const normalize = (value) => (value ?? "").replace(/\s+/g, " ").trim();
|
|
51
|
+
const unique = (items) => Array.from(new Set(items));
|
|
52
|
+
const compilePattern = (selector, kind) => {
|
|
53
|
+
const value = kind === "value" ? selector.value : selector.name ?? "";
|
|
54
|
+
const isRegex = kind === "value" ? selector.isRegex : selector.nameIsRegex;
|
|
55
|
+
const flags = kind === "value" ? selector.regexFlags : selector.nameRegexFlags;
|
|
56
|
+
if (isRegex) {
|
|
57
|
+
return new RegExp(value, flags ?? "");
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
};
|
|
61
|
+
const matchesPattern = (candidate, selector, kind) => {
|
|
62
|
+
const pattern = compilePattern(selector, kind);
|
|
63
|
+
const normalizedCandidate = normalize(candidate);
|
|
64
|
+
if (pattern instanceof RegExp) {
|
|
65
|
+
return pattern.test(normalizedCandidate);
|
|
66
|
+
}
|
|
67
|
+
if (selector.exact) {
|
|
68
|
+
return normalizedCandidate === pattern;
|
|
69
|
+
}
|
|
70
|
+
return normalizedCandidate.toLowerCase().includes(pattern.toLowerCase());
|
|
71
|
+
};
|
|
72
|
+
const implicitRole = (element) => {
|
|
73
|
+
const tagName = element.tagName.toLowerCase();
|
|
74
|
+
if (tagName === "button")
|
|
75
|
+
return "button";
|
|
76
|
+
if (tagName === "a" && element.hasAttribute("href"))
|
|
77
|
+
return "link";
|
|
78
|
+
if (tagName === "textarea")
|
|
79
|
+
return "textbox";
|
|
80
|
+
if (tagName === "select") {
|
|
81
|
+
return element.hasAttribute("multiple") ? "listbox" : "combobox";
|
|
82
|
+
}
|
|
83
|
+
if (tagName === "img")
|
|
84
|
+
return "img";
|
|
85
|
+
if (tagName !== "input")
|
|
86
|
+
return null;
|
|
87
|
+
const type = (element.getAttribute("type") ?? "text").toLowerCase();
|
|
88
|
+
switch (type) {
|
|
89
|
+
case "button":
|
|
90
|
+
case "submit":
|
|
91
|
+
case "reset":
|
|
92
|
+
return "button";
|
|
93
|
+
case "checkbox":
|
|
94
|
+
return "checkbox";
|
|
95
|
+
case "radio":
|
|
96
|
+
return "radio";
|
|
97
|
+
case "range":
|
|
98
|
+
return "slider";
|
|
99
|
+
case "email":
|
|
100
|
+
case "password":
|
|
101
|
+
case "search":
|
|
102
|
+
case "tel":
|
|
103
|
+
case "text":
|
|
104
|
+
case "url":
|
|
105
|
+
return "textbox";
|
|
106
|
+
default:
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const roleOf = (element) => normalize(element.getAttribute("role")) || implicitRole(element);
|
|
111
|
+
const accessibleName = (element) => {
|
|
112
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
113
|
+
if (labelledBy) {
|
|
114
|
+
const text = labelledBy
|
|
115
|
+
.split(/\s+/)
|
|
116
|
+
.map((id) => document.getElementById(id))
|
|
117
|
+
.filter((node) => Boolean(node))
|
|
118
|
+
.map((node) => normalize(node.innerText || node.textContent))
|
|
119
|
+
.join(" ");
|
|
120
|
+
if (text)
|
|
121
|
+
return normalize(text);
|
|
122
|
+
}
|
|
123
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
124
|
+
if (ariaLabel)
|
|
125
|
+
return normalize(ariaLabel);
|
|
126
|
+
if (element instanceof HTMLInputElement &&
|
|
127
|
+
["button", "submit", "reset"].includes(element.type)) {
|
|
128
|
+
return normalize(element.value);
|
|
129
|
+
}
|
|
130
|
+
return normalize(element.innerText || element.textContent);
|
|
131
|
+
};
|
|
132
|
+
const textSelectorValue = (element) => {
|
|
133
|
+
if (element instanceof HTMLInputElement &&
|
|
134
|
+
["button", "submit", "reset"].includes(element.type)) {
|
|
135
|
+
return element.value;
|
|
136
|
+
}
|
|
137
|
+
return element.innerText || element.textContent || "";
|
|
138
|
+
};
|
|
139
|
+
const shouldSkipTextSelectorElement = (element) => {
|
|
140
|
+
const tagName = element.tagName.toLowerCase();
|
|
141
|
+
return tagName === "head" || tagName === "title" || tagName === "script" || tagName === "style";
|
|
142
|
+
};
|
|
143
|
+
const candidatesFromRoot = (root, selector) => {
|
|
144
|
+
if (selector.strategy === "css") {
|
|
145
|
+
const matches = [];
|
|
146
|
+
if (root instanceof Element && root.matches(selector.value)) {
|
|
147
|
+
matches.push(root);
|
|
148
|
+
}
|
|
149
|
+
matches.push(...Array.from(root.querySelectorAll(selector.value)));
|
|
150
|
+
return unique(matches);
|
|
151
|
+
}
|
|
152
|
+
const descendants = root instanceof Document
|
|
153
|
+
? [root.documentElement, ...Array.from(root.querySelectorAll("*"))]
|
|
154
|
+
: [root, ...Array.from(root.querySelectorAll("*"))];
|
|
155
|
+
if (selector.strategy === "text") {
|
|
156
|
+
const matching = descendants.filter((element) => {
|
|
157
|
+
if (shouldSkipTextSelectorElement(element)) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return matchesPattern(textSelectorValue(element), selector, "value");
|
|
161
|
+
});
|
|
162
|
+
return matching.filter((element) => !Array.from(element.querySelectorAll("*")).some((child) => !shouldSkipTextSelectorElement(child) &&
|
|
163
|
+
matchesPattern(textSelectorValue(child), selector, "value")));
|
|
164
|
+
}
|
|
165
|
+
return descendants.filter((element) => {
|
|
166
|
+
if (roleOf(element) !== selector.value) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (selector.name === undefined && !selector.nameIsRegex) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return matchesPattern(accessibleName(element), selector, "name");
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
const resolveElements = () => {
|
|
176
|
+
let current = [document];
|
|
177
|
+
for (const selector of payload.chain) {
|
|
178
|
+
current = unique(current.flatMap((root) => candidatesFromRoot(root, selector)));
|
|
179
|
+
}
|
|
180
|
+
let elements = current.filter((node) => node instanceof HTMLElement);
|
|
181
|
+
if (payload.pick?.kind === "first") {
|
|
182
|
+
elements = elements.slice(0, 1);
|
|
183
|
+
}
|
|
184
|
+
else if (payload.pick?.kind === "last") {
|
|
185
|
+
elements = elements.slice(-1);
|
|
186
|
+
}
|
|
187
|
+
else if (payload.pick?.kind === "nth") {
|
|
188
|
+
const pickedElement = elements[payload.pick.index];
|
|
189
|
+
elements = pickedElement ? [pickedElement] : [];
|
|
190
|
+
}
|
|
191
|
+
return elements;
|
|
192
|
+
};
|
|
193
|
+
const isVisible = (element) => {
|
|
194
|
+
const style = window.getComputedStyle(element);
|
|
195
|
+
const rect = element.getBoundingClientRect();
|
|
196
|
+
return (style.visibility !== "hidden" &&
|
|
197
|
+
style.display !== "none" &&
|
|
198
|
+
Number.parseFloat(style.opacity || "1") !== 0 &&
|
|
199
|
+
rect.width > 0 &&
|
|
200
|
+
rect.height > 0);
|
|
201
|
+
};
|
|
202
|
+
const hasVisibleStyle = (element) => {
|
|
203
|
+
let current = element;
|
|
204
|
+
while (current) {
|
|
205
|
+
const style = window.getComputedStyle(current);
|
|
206
|
+
if (style.visibility === "hidden" ||
|
|
207
|
+
style.display === "none" ||
|
|
208
|
+
Number.parseFloat(style.opacity || "1") === 0) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
current = current.parentElement;
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
};
|
|
215
|
+
const chooseActionRect = (element) => {
|
|
216
|
+
const viewport = {
|
|
217
|
+
bottom: window.innerHeight,
|
|
218
|
+
left: 0,
|
|
219
|
+
right: window.innerWidth,
|
|
220
|
+
top: 0
|
|
221
|
+
};
|
|
222
|
+
const intersect = (rect) => {
|
|
223
|
+
const left = Math.max(rect.left, viewport.left);
|
|
224
|
+
const right = Math.min(rect.right, viewport.right);
|
|
225
|
+
const top = Math.max(rect.top, viewport.top);
|
|
226
|
+
const bottom = Math.min(rect.bottom, viewport.bottom);
|
|
227
|
+
if (right - left <= 0 || bottom - top <= 0) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
return new DOMRect(left, top, right - left, bottom - top);
|
|
231
|
+
};
|
|
232
|
+
for (const rect of Array.from(element.getClientRects())) {
|
|
233
|
+
const visiblePart = intersect(rect);
|
|
234
|
+
if (visiblePart && visiblePart.width * visiblePart.height > 0.99) {
|
|
235
|
+
return visiblePart;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const visibleBoundingBox = intersect(element.getBoundingClientRect());
|
|
239
|
+
return visibleBoundingBox && visibleBoundingBox.width * visibleBoundingBox.height > 0.99 ? visibleBoundingBox : null;
|
|
240
|
+
};
|
|
241
|
+
const isDisabled = (element) => {
|
|
242
|
+
if (element instanceof HTMLButtonElement ||
|
|
243
|
+
element instanceof HTMLInputElement ||
|
|
244
|
+
element instanceof HTMLSelectElement ||
|
|
245
|
+
element instanceof HTMLTextAreaElement ||
|
|
246
|
+
element instanceof HTMLOptGroupElement ||
|
|
247
|
+
element instanceof HTMLOptionElement ||
|
|
248
|
+
element instanceof HTMLFieldSetElement) {
|
|
249
|
+
return element.disabled;
|
|
250
|
+
}
|
|
251
|
+
return element.getAttribute("aria-disabled") === "true";
|
|
252
|
+
};
|
|
253
|
+
const isEditable = (element) => {
|
|
254
|
+
if (element instanceof HTMLInputElement ||
|
|
255
|
+
element instanceof HTMLTextAreaElement ||
|
|
256
|
+
element instanceof HTMLSelectElement) {
|
|
257
|
+
return !element.hasAttribute("readonly") && !isDisabled(element);
|
|
258
|
+
}
|
|
259
|
+
if (element instanceof HTMLElement && element.isContentEditable) {
|
|
260
|
+
return !isDisabled(element);
|
|
261
|
+
}
|
|
262
|
+
const ariaReadonlyRoles = new Set([
|
|
263
|
+
"checkbox",
|
|
264
|
+
"combobox",
|
|
265
|
+
"grid",
|
|
266
|
+
"gridcell",
|
|
267
|
+
"listbox",
|
|
268
|
+
"radiogroup",
|
|
269
|
+
"slider",
|
|
270
|
+
"spinbutton",
|
|
271
|
+
"textbox",
|
|
272
|
+
"columnheader",
|
|
273
|
+
"rowheader",
|
|
274
|
+
"searchbox",
|
|
275
|
+
"switch",
|
|
276
|
+
"treegrid"
|
|
277
|
+
]);
|
|
278
|
+
if (ariaReadonlyRoles.has(element.getAttribute("role") ?? "")) {
|
|
279
|
+
return !isDisabled(element) && element.getAttribute("aria-readonly") !== "true";
|
|
280
|
+
}
|
|
281
|
+
throw new Error("Element is not an <input>, <textarea>, <select> or [contenteditable] and does not have a role allowing [aria-readonly]");
|
|
282
|
+
};
|
|
283
|
+
const fillActionabilityError = (element) => {
|
|
284
|
+
if (!payload.force && !isVisible(element)) {
|
|
285
|
+
return "Element is not visible.";
|
|
286
|
+
}
|
|
287
|
+
if (!payload.force && isDisabled(element)) {
|
|
288
|
+
return "Element is not enabled.";
|
|
289
|
+
}
|
|
290
|
+
if (!payload.force && !isEditable(element)) {
|
|
291
|
+
return "Element is not editable.";
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
};
|
|
295
|
+
const waitForFillActionability = (element) => {
|
|
296
|
+
const assertActionable = () => {
|
|
297
|
+
const error = fillActionabilityError(element);
|
|
298
|
+
if (error) {
|
|
299
|
+
throw new Error(error);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
if (payload.force || !payload.timeoutMs || payload.timeoutMs <= 0) {
|
|
303
|
+
assertActionable();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
return new Promise((resolve, reject) => {
|
|
307
|
+
const deadline = Date.now() + payload.timeoutMs;
|
|
308
|
+
const tick = () => {
|
|
309
|
+
try {
|
|
310
|
+
assertActionable();
|
|
311
|
+
resolve();
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (Date.now() + 50 > deadline) {
|
|
315
|
+
reject(error);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
setTimeout(tick, 50);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
tick();
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
const fillInputValue = (input, value) => {
|
|
325
|
+
const type = input.type.toLowerCase();
|
|
326
|
+
const inputTypesToSetValue = new Set(["color", "date", "time", "datetime-local", "month", "range", "week"]);
|
|
327
|
+
const inputTypesToTypeInto = new Set(["", "email", "number", "password", "search", "tel", "text", "url"]);
|
|
328
|
+
if (!inputTypesToTypeInto.has(type) && !inputTypesToSetValue.has(type)) {
|
|
329
|
+
throw new Error(`Input of type "${type}" cannot be filled`);
|
|
330
|
+
}
|
|
331
|
+
if (type === "number") {
|
|
332
|
+
value = value.trim();
|
|
333
|
+
if (isNaN(Number(value))) {
|
|
334
|
+
throw new Error("Cannot type text into input[type=number]");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (type === "color") {
|
|
338
|
+
value = value.toLowerCase();
|
|
339
|
+
}
|
|
340
|
+
if (inputTypesToSetValue.has(type)) {
|
|
341
|
+
value = value.trim();
|
|
342
|
+
input.value = value;
|
|
343
|
+
if (input.value !== value) {
|
|
344
|
+
throw new Error("Malformed value");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return value;
|
|
348
|
+
};
|
|
349
|
+
const firstElement = resolveElements()[0] ?? null;
|
|
350
|
+
switch (payload.operation) {
|
|
351
|
+
case "textContent":
|
|
352
|
+
return firstElement ? firstElement.textContent : null;
|
|
353
|
+
case "isVisible":
|
|
354
|
+
return firstElement ? isVisible(firstElement) : false;
|
|
355
|
+
case "focus":
|
|
356
|
+
if (!firstElement) {
|
|
357
|
+
throw new Error("No element found for locator.");
|
|
358
|
+
}
|
|
359
|
+
firstElement.focus();
|
|
360
|
+
return true;
|
|
361
|
+
case "fill":
|
|
362
|
+
if (!firstElement) {
|
|
363
|
+
throw new Error("No element found for locator.");
|
|
364
|
+
}
|
|
365
|
+
{
|
|
366
|
+
const fillElement = () => {
|
|
367
|
+
firstElement.focus();
|
|
368
|
+
if (firstElement instanceof HTMLInputElement) {
|
|
369
|
+
firstElement.value = fillInputValue(firstElement, payload.value ?? "");
|
|
370
|
+
}
|
|
371
|
+
else if (firstElement instanceof HTMLTextAreaElement) {
|
|
372
|
+
firstElement.value = payload.value ?? "";
|
|
373
|
+
}
|
|
374
|
+
else if (firstElement.isContentEditable) {
|
|
375
|
+
firstElement.textContent = payload.value ?? "";
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
throw new Error("Element is not an <input>, <textarea> or [contenteditable] element");
|
|
379
|
+
}
|
|
380
|
+
firstElement.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
381
|
+
firstElement.dispatchEvent(new Event("change", { bubbles: true }));
|
|
382
|
+
return true;
|
|
383
|
+
};
|
|
384
|
+
if (payload.timeoutMs !== undefined) {
|
|
385
|
+
const waitResult = waitForFillActionability(firstElement);
|
|
386
|
+
return waitResult instanceof Promise ? waitResult.then(fillElement) : fillElement();
|
|
387
|
+
}
|
|
388
|
+
const error = fillActionabilityError(firstElement);
|
|
389
|
+
if (error) {
|
|
390
|
+
throw new Error(error);
|
|
391
|
+
}
|
|
392
|
+
return fillElement();
|
|
393
|
+
}
|
|
394
|
+
case "actionPoint":
|
|
395
|
+
if (!firstElement) {
|
|
396
|
+
throw new Error("No element found for locator.");
|
|
397
|
+
}
|
|
398
|
+
firstElement.scrollIntoView({
|
|
399
|
+
block: "center",
|
|
400
|
+
inline: "center",
|
|
401
|
+
behavior: "instant"
|
|
402
|
+
});
|
|
403
|
+
if (!payload.force && !hasVisibleStyle(firstElement)) {
|
|
404
|
+
throw new Error("Element is not visible.");
|
|
405
|
+
}
|
|
406
|
+
const rect = chooseActionRect(firstElement);
|
|
407
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
408
|
+
throw new Error("Element is outside of the viewport.");
|
|
409
|
+
}
|
|
410
|
+
const offsetX = payload.position ? payload.position.x : rect.width / 2;
|
|
411
|
+
const offsetY = payload.position ? payload.position.y : rect.height / 2;
|
|
412
|
+
return {
|
|
413
|
+
x: rect.left + offsetX,
|
|
414
|
+
y: rect.top + offsetY
|
|
415
|
+
};
|
|
416
|
+
default:
|
|
417
|
+
throw new Error(`Unsupported locator operation: ${payload.operation}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const LOCATOR_OPERATION_SOURCE = locatorOperation.toString();
|
|
421
|
+
export class BidiBrowserAdapterFactory {
|
|
422
|
+
create(options) {
|
|
423
|
+
return new BidiBrowserAdapter(options);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
class BidiBrowserAdapter {
|
|
427
|
+
options;
|
|
428
|
+
protocol = "bidi";
|
|
429
|
+
capabilities = BIDI_CAPABILITIES;
|
|
430
|
+
client;
|
|
431
|
+
ownsSession = false;
|
|
432
|
+
spawnedProcess;
|
|
433
|
+
unregisterTestBrowserProcess;
|
|
434
|
+
userDataDir;
|
|
435
|
+
constructor(options) {
|
|
436
|
+
this.options = options;
|
|
437
|
+
}
|
|
438
|
+
async connect() {
|
|
439
|
+
if (this.client) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (this.options.browserName !== "firefox") {
|
|
443
|
+
throw new Error('The BiDi backend currently only supports browserName "firefox".');
|
|
444
|
+
}
|
|
445
|
+
if (this.options.wsEndpoint) {
|
|
446
|
+
const connection = await connectBidiFromWsEndpoint(this.options.wsEndpoint, this.options.sessionId);
|
|
447
|
+
this.client = connection.client;
|
|
448
|
+
this.ownsSession = connection.ownsSession;
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const { client, ownsSession, process: proc, unregisterTestBrowserProcess, userDataDir } = await launchFirefoxBidi(this.options);
|
|
452
|
+
this.client = client;
|
|
453
|
+
this.ownsSession = ownsSession;
|
|
454
|
+
this.spawnedProcess = proc;
|
|
455
|
+
this.unregisterTestBrowserProcess = unregisterTestBrowserProcess;
|
|
456
|
+
this.userDataDir = userDataDir;
|
|
457
|
+
}
|
|
458
|
+
async browser() {
|
|
459
|
+
if (!this.client) {
|
|
460
|
+
throw new Error("BiDi browser adapter is not connected.");
|
|
461
|
+
}
|
|
462
|
+
return new BidiBrowserSession(this.client, this.ownsSession);
|
|
463
|
+
}
|
|
464
|
+
async close() {
|
|
465
|
+
const client = this.client;
|
|
466
|
+
this.client = undefined;
|
|
467
|
+
this.ownsSession = false;
|
|
468
|
+
try {
|
|
469
|
+
if (client) {
|
|
470
|
+
client.close();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
finally {
|
|
474
|
+
if (this.spawnedProcess) {
|
|
475
|
+
await cleanupFirefoxProcess(this.spawnedProcess, this.userDataDir, this.unregisterTestBrowserProcess);
|
|
476
|
+
}
|
|
477
|
+
this.spawnedProcess = undefined;
|
|
478
|
+
this.unregisterTestBrowserProcess = undefined;
|
|
479
|
+
this.userDataDir = undefined;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
class BidiBrowserSession {
|
|
484
|
+
client;
|
|
485
|
+
ownsSession;
|
|
486
|
+
constructor(client, ownsSession) {
|
|
487
|
+
this.client = client;
|
|
488
|
+
this.ownsSession = ownsSession;
|
|
489
|
+
}
|
|
490
|
+
async version() {
|
|
491
|
+
const browserName = this.client.capabilities.browserName ?? "firefox";
|
|
492
|
+
const browserVersion = this.client.capabilities.browserVersion;
|
|
493
|
+
return browserVersion ? `${browserName}/${browserVersion}` : browserName;
|
|
494
|
+
}
|
|
495
|
+
async newContext(options = {}) {
|
|
496
|
+
if (options.reuseDefaultUserContext) {
|
|
497
|
+
return new BidiBrowserContextAdapter(this.client, undefined, options);
|
|
498
|
+
}
|
|
499
|
+
const response = await this.client.browserCreateUserContext({});
|
|
500
|
+
return new BidiBrowserContextAdapter(this.client, response.userContext, options);
|
|
501
|
+
}
|
|
502
|
+
async close() {
|
|
503
|
+
if (this.ownsSession) {
|
|
504
|
+
await this.client.sessionEnd({});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
class BidiBrowserContextAdapter {
|
|
509
|
+
client;
|
|
510
|
+
userContext;
|
|
511
|
+
options;
|
|
512
|
+
pages = new Set();
|
|
513
|
+
initScripts = new Set();
|
|
514
|
+
constructor(client, userContext, options) {
|
|
515
|
+
this.client = client;
|
|
516
|
+
this.userContext = userContext;
|
|
517
|
+
this.options = options;
|
|
518
|
+
}
|
|
519
|
+
async newPage() {
|
|
520
|
+
const response = await this.client.browsingContextCreate(this.userContext
|
|
521
|
+
? {
|
|
522
|
+
type: "window",
|
|
523
|
+
userContext: this.userContext
|
|
524
|
+
}
|
|
525
|
+
: {
|
|
526
|
+
// Reuse the already-open RoxyBrowser window, but create a new tab
|
|
527
|
+
// instead of hijacking the internal start page or opening a second window.
|
|
528
|
+
type: "tab"
|
|
529
|
+
});
|
|
530
|
+
let page;
|
|
531
|
+
page = await BidiPageAdapter.create(this.client, response.context, this.options, () => {
|
|
532
|
+
this.pages.delete(page);
|
|
533
|
+
});
|
|
534
|
+
for (const entry of this.initScripts) {
|
|
535
|
+
if (entry.scriptId) {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
const disposable = await page.addInitScript(entry.source);
|
|
539
|
+
entry.pageDisposables.set(page, disposable);
|
|
540
|
+
}
|
|
541
|
+
this.pages.add(page);
|
|
542
|
+
return page;
|
|
543
|
+
}
|
|
544
|
+
async addInitScript(source, _arg) {
|
|
545
|
+
const entry = {
|
|
546
|
+
source,
|
|
547
|
+
pageDisposables: new WeakMap(),
|
|
548
|
+
scriptId: undefined
|
|
549
|
+
};
|
|
550
|
+
this.initScripts.add(entry);
|
|
551
|
+
try {
|
|
552
|
+
if (this.userContext) {
|
|
553
|
+
const result = await this.client.scriptAddPreloadScript({
|
|
554
|
+
functionDeclaration: `() => { return ${source} }`,
|
|
555
|
+
userContexts: [this.userContext]
|
|
556
|
+
});
|
|
557
|
+
entry.scriptId = result.script;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
await Promise.all(Array.from(this.pages, async (page) => {
|
|
561
|
+
const disposable = await page.addInitScript(source);
|
|
562
|
+
entry.pageDisposables.set(page, disposable);
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
this.initScripts.delete(entry);
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
dispose: async () => {
|
|
572
|
+
if (!this.initScripts.delete(entry)) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
await Promise.all(Array.from(this.pages, async (page) => {
|
|
576
|
+
await entry.pageDisposables.get(page)?.dispose();
|
|
577
|
+
}));
|
|
578
|
+
if (entry.scriptId) {
|
|
579
|
+
await this.client.scriptRemovePreloadScript({ script: entry.scriptId }).catch(() => { });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
async setExtraHTTPHeaders(headers) {
|
|
585
|
+
this.options.extraHTTPHeaders = { ...headers };
|
|
586
|
+
await Promise.all(Array.from(this.pages.values()).map(async (page) => {
|
|
587
|
+
await page.updateContextExtraHTTPHeaders();
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
async close() {
|
|
591
|
+
this.pages.clear();
|
|
592
|
+
if (!this.userContext) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
await this.client.browserRemoveUserContext({
|
|
596
|
+
userContext: this.userContext
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
class BidiPageAdapter {
|
|
601
|
+
client;
|
|
602
|
+
contextId;
|
|
603
|
+
contextOptions;
|
|
604
|
+
onClosed;
|
|
605
|
+
closePromise;
|
|
606
|
+
resolveClosePromise;
|
|
607
|
+
closePromiseResolved = false;
|
|
608
|
+
closed = false;
|
|
609
|
+
closeReason;
|
|
610
|
+
currentUrl = "about:blank";
|
|
611
|
+
domContentLoaded = false;
|
|
612
|
+
loadFired = false;
|
|
613
|
+
sameDocumentNavigation = false;
|
|
614
|
+
allowSameDocumentNavigationToResolveWaiters = false;
|
|
615
|
+
responseDataCollector;
|
|
616
|
+
navigationResponseCapture;
|
|
617
|
+
stateWaiters = new Set();
|
|
618
|
+
eventListeners = new Map();
|
|
619
|
+
bidiListeners = new Map();
|
|
620
|
+
currentViewportSize = null;
|
|
621
|
+
currentMousePosition = { x: 0, y: 0 };
|
|
622
|
+
lastMouseButton = "none";
|
|
623
|
+
pressedMouseButtons = new Set();
|
|
624
|
+
pressedKeyboardModifiers = new Set();
|
|
625
|
+
pageExtraHTTPHeaders;
|
|
626
|
+
screencastActionOptions = null;
|
|
627
|
+
screencastActionAnnotation = null;
|
|
628
|
+
screencastActionAbortController = null;
|
|
629
|
+
screencastOverlaysVisible = true;
|
|
630
|
+
screencastOverlayId = 0;
|
|
631
|
+
screencastOverlays = new Map();
|
|
632
|
+
static async create(client, contextId, contextOptions, onClosed) {
|
|
633
|
+
const page = new BidiPageAdapter(client, contextId, contextOptions, onClosed);
|
|
634
|
+
await page.initialize();
|
|
635
|
+
return page;
|
|
636
|
+
}
|
|
637
|
+
constructor(client, contextId, contextOptions, onClosed) {
|
|
638
|
+
this.client = client;
|
|
639
|
+
this.contextId = contextId;
|
|
640
|
+
this.contextOptions = contextOptions;
|
|
641
|
+
this.onClosed = onClosed;
|
|
642
|
+
this.closePromise = new Promise((resolve) => {
|
|
643
|
+
this.resolveClosePromise = resolve;
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
async initialize() {
|
|
647
|
+
await this.client.sessionSubscribe({
|
|
648
|
+
contexts: [this.contextId],
|
|
649
|
+
events: [
|
|
650
|
+
"browsingContext.contextDestroyed",
|
|
651
|
+
"browsingContext.domContentLoaded",
|
|
652
|
+
"browsingContext.fragmentNavigated",
|
|
653
|
+
"browsingContext.historyUpdated",
|
|
654
|
+
"browsingContext.load",
|
|
655
|
+
"browsingContext.userPromptOpened",
|
|
656
|
+
"log.entryAdded",
|
|
657
|
+
"network.beforeRequestSent",
|
|
658
|
+
"network.responseCompleted",
|
|
659
|
+
"network.fetchError",
|
|
660
|
+
"network.responseStarted"
|
|
661
|
+
]
|
|
662
|
+
});
|
|
663
|
+
const collectorResult = await this.client.networkAddDataCollector({
|
|
664
|
+
contexts: [this.contextId],
|
|
665
|
+
dataTypes: ["response"],
|
|
666
|
+
maxEncodedDataSize: 10_000_000
|
|
667
|
+
});
|
|
668
|
+
this.responseDataCollector = collectorResult.collector;
|
|
669
|
+
this.attachBiDiListeners();
|
|
670
|
+
await this.applyContextOptions();
|
|
671
|
+
}
|
|
672
|
+
async goto(url, options = {}) {
|
|
673
|
+
const waitUntil = verifyLifecycle("waitUntil", options.waitUntil ?? "load");
|
|
674
|
+
const targetUrl = completeUserURL(url);
|
|
675
|
+
this.resolveNavigationReferer(options, targetUrl);
|
|
676
|
+
const capture = this.beginNavigationResponseCapture();
|
|
677
|
+
this.resetNavigationState();
|
|
678
|
+
try {
|
|
679
|
+
await this.client.browsingContextNavigate({
|
|
680
|
+
context: this.contextId,
|
|
681
|
+
url: targetUrl,
|
|
682
|
+
wait: waitUntil === "domcontentloaded" ? "interactive" : "complete"
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
687
|
+
if (!message.includes("blockedByPolicy")) {
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
await this.navigateViaLocation(targetUrl);
|
|
691
|
+
}
|
|
692
|
+
this.currentUrl = targetUrl;
|
|
693
|
+
if (waitUntil !== "commit") {
|
|
694
|
+
await this.waitForLoadState(waitUntil, options.timeout);
|
|
695
|
+
}
|
|
696
|
+
if (this.navigationResponseCapture === capture) {
|
|
697
|
+
this.navigationResponseCapture = undefined;
|
|
698
|
+
}
|
|
699
|
+
return capture.lastResponse;
|
|
700
|
+
}
|
|
701
|
+
url() {
|
|
702
|
+
return this.currentUrl;
|
|
703
|
+
}
|
|
704
|
+
async goBack(options = {}) {
|
|
705
|
+
return this.navigateHistory(-1, options);
|
|
706
|
+
}
|
|
707
|
+
async goForward(options = {}) {
|
|
708
|
+
return this.navigateHistory(1, options);
|
|
709
|
+
}
|
|
710
|
+
async reload(options = {}) {
|
|
711
|
+
const waitUntil = verifyLifecycle("waitUntil", options.waitUntil ?? "load");
|
|
712
|
+
const capture = this.beginNavigationResponseCapture();
|
|
713
|
+
this.resetNavigationState();
|
|
714
|
+
await this.client.browsingContextReload({
|
|
715
|
+
context: this.contextId,
|
|
716
|
+
wait: waitUntil === "domcontentloaded" ? "interactive" : "complete"
|
|
717
|
+
});
|
|
718
|
+
if (waitUntil !== "commit") {
|
|
719
|
+
await this.waitForLoadState(waitUntil, options.timeout);
|
|
720
|
+
}
|
|
721
|
+
if (this.navigationResponseCapture === capture) {
|
|
722
|
+
this.navigationResponseCapture = undefined;
|
|
723
|
+
}
|
|
724
|
+
return capture.lastResponse;
|
|
725
|
+
}
|
|
726
|
+
async title() {
|
|
727
|
+
return this.evaluateExpression("document.title");
|
|
728
|
+
}
|
|
729
|
+
async content() {
|
|
730
|
+
return this.evaluateExpression(`(() => {
|
|
731
|
+
const doctype = document.doctype ? new XMLSerializer().serializeToString(document.doctype) : "";
|
|
732
|
+
const documentElement = document.documentElement.cloneNode(true);
|
|
733
|
+
if (documentElement instanceof Element) {
|
|
734
|
+
documentElement.querySelectorAll([
|
|
735
|
+
"#__roxy_screencast_actions_style__",
|
|
736
|
+
"#__roxy_screencast_overlay_style__",
|
|
737
|
+
"x-pw-action-overlays",
|
|
738
|
+
"x-pw-user-overlays",
|
|
739
|
+
"[data-roxy-highlight-overlay]"
|
|
740
|
+
].join(",")).forEach((node) => node.remove());
|
|
741
|
+
}
|
|
742
|
+
return doctype + documentElement.outerHTML;
|
|
743
|
+
})()`);
|
|
744
|
+
}
|
|
745
|
+
async setContent(html, options = {}) {
|
|
746
|
+
const waitUntil = verifyLifecycle("waitUntil", options.waitUntil ?? "load");
|
|
747
|
+
this.resetNavigationState();
|
|
748
|
+
await this.evaluateFunction(`(payload) => {
|
|
749
|
+
document.open();
|
|
750
|
+
document.write(payload.html);
|
|
751
|
+
document.close();
|
|
752
|
+
}`, { html });
|
|
753
|
+
await this.evaluateFunction(HYDRATE_DECLARATIVE_SHADOW_ROOTS_SOURCE);
|
|
754
|
+
if (waitUntil !== "commit") {
|
|
755
|
+
await this.waitForLoadState(waitUntil, options.timeout);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async addInitScript(source, _arg) {
|
|
759
|
+
const result = await this.client.scriptAddPreloadScript({
|
|
760
|
+
functionDeclaration: `() => { ${source} }`,
|
|
761
|
+
contexts: [this.contextId]
|
|
762
|
+
});
|
|
763
|
+
const script = result.script;
|
|
764
|
+
return {
|
|
765
|
+
dispose: async () => {
|
|
766
|
+
if (!script) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
await this.client.scriptRemovePreloadScript({ script }).catch(() => { });
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
async navigateViaLocation(url) {
|
|
774
|
+
await this.evaluateFunction(`(payload) => {
|
|
775
|
+
globalThis.location.href = payload.url;
|
|
776
|
+
}`, { url });
|
|
777
|
+
}
|
|
778
|
+
async evaluate(expression, arg) {
|
|
779
|
+
// Convert function to string if needed (handles cases where a function is passed instead of string)
|
|
780
|
+
const expressionStr = typeof expression === "function"
|
|
781
|
+
? expression.toString()
|
|
782
|
+
: expression;
|
|
783
|
+
if (arg === undefined && !looksLikeFunctionExpression(expressionStr)) {
|
|
784
|
+
return this.evaluateExpression(expressionStr);
|
|
785
|
+
}
|
|
786
|
+
return this.evaluateFunction(expressionStr, arg);
|
|
787
|
+
}
|
|
788
|
+
async addScriptTag(options) {
|
|
789
|
+
await this.evaluateFunction(`async (payload) => {
|
|
790
|
+
const script = document.createElement('script');
|
|
791
|
+
script.type = payload.type || 'text/javascript';
|
|
792
|
+
if (payload.url) {
|
|
793
|
+
script.src = payload.url;
|
|
794
|
+
const promise = new Promise((resolve, reject) => {
|
|
795
|
+
script.onload = resolve;
|
|
796
|
+
script.onerror = event => reject(typeof event === 'string' ? new Error(event) : new Error('Failed to load script at ' + script.src));
|
|
797
|
+
});
|
|
798
|
+
document.head.appendChild(script);
|
|
799
|
+
await promise;
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
script.text = payload.content || '';
|
|
803
|
+
let error = null;
|
|
804
|
+
script.onerror = event => error = event;
|
|
805
|
+
document.head.appendChild(script);
|
|
806
|
+
if (error)
|
|
807
|
+
throw error;
|
|
808
|
+
}`, options ?? {});
|
|
809
|
+
return new BidiElementHandleAdapter(this, {
|
|
810
|
+
chain: [{ strategy: "css", value: "script:last-of-type" }],
|
|
811
|
+
pick: { kind: "first" }
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
async addStyleTag(options) {
|
|
815
|
+
await this.evaluateFunction(`async (payload) => {
|
|
816
|
+
const element = document.createElement(payload.url ? 'link' : 'style');
|
|
817
|
+
if (payload.url) {
|
|
818
|
+
element.rel = 'stylesheet';
|
|
819
|
+
element.href = payload.url;
|
|
820
|
+
const promise = new Promise((resolve, reject) => {
|
|
821
|
+
element.onload = resolve;
|
|
822
|
+
element.onerror = event => reject(typeof event === 'string' ? new Error(event) : new Error('Failed to load stylesheet at ' + element.href));
|
|
823
|
+
});
|
|
824
|
+
document.head.appendChild(element);
|
|
825
|
+
await promise;
|
|
826
|
+
return;
|
|
827
|
+
} else {
|
|
828
|
+
element.type = 'text/css';
|
|
829
|
+
element.appendChild(document.createTextNode(payload.content || ''));
|
|
830
|
+
const promise = new Promise((resolve, reject) => {
|
|
831
|
+
element.onload = resolve;
|
|
832
|
+
element.onerror = reject;
|
|
833
|
+
});
|
|
834
|
+
document.head.appendChild(element);
|
|
835
|
+
await promise;
|
|
836
|
+
}
|
|
837
|
+
}`, options ?? {});
|
|
838
|
+
return new BidiElementHandleAdapter(this, {
|
|
839
|
+
chain: [{ strategy: "css", value: "style:last-of-type,link[rel=stylesheet]:last-of-type" }],
|
|
840
|
+
pick: { kind: "first" }
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
async waitForLoadState(state = "load", timeout = 30_000, _frameId) {
|
|
844
|
+
return this.waitForLoadStateInternal(state, timeout, false);
|
|
845
|
+
}
|
|
846
|
+
async waitForPendingLoadState(state = "load", timeout = 30_000, previousUrl) {
|
|
847
|
+
return this.waitForLoadStateInternal(state, timeout, true, previousUrl);
|
|
848
|
+
}
|
|
849
|
+
async waitForLoadStateInternal(state, timeout, skipCurrentDocumentReadyCheck, previousUrl) {
|
|
850
|
+
const targetState = verifyLifecycle("state", state ?? "load");
|
|
851
|
+
const stillOnPreviousUrl = previousUrl !== undefined && this.currentUrl === previousUrl;
|
|
852
|
+
if (targetState === "commit" || (this.isStateSatisfied(targetState) && !stillOnPreviousUrl)) {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (!skipCurrentDocumentReadyCheck && await this.isCurrentDocumentReadyFor(targetState)) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const waiterPromise = new Promise((resolve, reject) => {
|
|
859
|
+
const timer = timeout === 0
|
|
860
|
+
? null
|
|
861
|
+
: setTimeout(() => {
|
|
862
|
+
this.stateWaiters.delete(waiter);
|
|
863
|
+
reject(new TimeoutError(`page.waitForLoadState: Timeout ${timeout}ms exceeded.`));
|
|
864
|
+
}, timeout);
|
|
865
|
+
const waiter = {
|
|
866
|
+
state: targetState,
|
|
867
|
+
previousUrl,
|
|
868
|
+
startedAt: Date.now(),
|
|
869
|
+
resolve: () => {
|
|
870
|
+
if (timer) {
|
|
871
|
+
clearTimeout(timer);
|
|
872
|
+
}
|
|
873
|
+
this.stateWaiters.delete(waiter);
|
|
874
|
+
resolve();
|
|
875
|
+
},
|
|
876
|
+
reject: (error) => {
|
|
877
|
+
if (timer) {
|
|
878
|
+
clearTimeout(timer);
|
|
879
|
+
}
|
|
880
|
+
this.stateWaiters.delete(waiter);
|
|
881
|
+
reject(error);
|
|
882
|
+
},
|
|
883
|
+
timer
|
|
884
|
+
};
|
|
885
|
+
this.stateWaiters.add(waiter);
|
|
886
|
+
this.flushWaiters();
|
|
887
|
+
});
|
|
888
|
+
await waiterPromise;
|
|
889
|
+
}
|
|
890
|
+
async ariaSnapshot(options = {}) {
|
|
891
|
+
const normalizedOptions = normalizeAriaSnapshotOptions(options);
|
|
892
|
+
const result = await withOptionalTimeout(this.evaluateFunction(ARIA_SNAPSHOT_EVALUATE_SOURCE, {
|
|
893
|
+
options: normalizedOptions
|
|
894
|
+
}), normalizedOptions.timeout, 'Timed out while generating page.ariaSnapshot().');
|
|
895
|
+
return result.text;
|
|
896
|
+
}
|
|
897
|
+
async resolveAriaRef(ref) {
|
|
898
|
+
const result = await this.evaluateFunction(ARIA_REF_SELECTOR_EVALUATE_SOURCE, { ref });
|
|
899
|
+
if (!result.ok) {
|
|
900
|
+
throw new Error(`Ref "${ref}" is not available on this page. Call page.ariaSnapshot({ mode: "ai" }) again first.`);
|
|
901
|
+
}
|
|
902
|
+
return {
|
|
903
|
+
ref: result.ref ?? ref,
|
|
904
|
+
selector: result.selector ?? null,
|
|
905
|
+
xpath: result.xpath ?? null,
|
|
906
|
+
querySelector: result.querySelector ?? null,
|
|
907
|
+
querySelectorChain: result.querySelectorChain ?? null,
|
|
908
|
+
framePath: result.framePath ?? [],
|
|
909
|
+
inShadowTree: Boolean(result.inShadowTree)
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
async setExtraHTTPHeaders(headers) {
|
|
913
|
+
this.pageExtraHTTPHeaders = { ...headers };
|
|
914
|
+
await this.updateExtraHTTPHeaders();
|
|
915
|
+
}
|
|
916
|
+
resolveNavigationReferer(options, targetUrl) {
|
|
917
|
+
const headers = mergeExtraHTTPHeaders(this.contextOptions.extraHTTPHeaders, this.pageExtraHTTPHeaders);
|
|
918
|
+
const headerEntry = Object.entries(headers)
|
|
919
|
+
.find(([name]) => name.toLowerCase() === "referer");
|
|
920
|
+
const headerReferer = headerEntry?.[1];
|
|
921
|
+
if (options.referer !== undefined && headerReferer !== undefined && headerReferer !== options.referer) {
|
|
922
|
+
throw new Error(`"referer" is already specified as extra HTTP header\n${targetUrl}`);
|
|
923
|
+
}
|
|
924
|
+
return options.referer ?? headerReferer;
|
|
925
|
+
}
|
|
926
|
+
async updateContextExtraHTTPHeaders() {
|
|
927
|
+
await this.updateExtraHTTPHeaders();
|
|
928
|
+
}
|
|
929
|
+
screenshotClipOrigin() {
|
|
930
|
+
return "viewport";
|
|
931
|
+
}
|
|
932
|
+
async screenshot(options = {}) {
|
|
933
|
+
const response = await this.client.browsingContextCaptureScreenshot({
|
|
934
|
+
context: this.contextId,
|
|
935
|
+
...(options.clip
|
|
936
|
+
? {
|
|
937
|
+
origin: "viewport",
|
|
938
|
+
clip: {
|
|
939
|
+
type: "box",
|
|
940
|
+
x: options.clip.x,
|
|
941
|
+
y: options.clip.y,
|
|
942
|
+
width: options.clip.width,
|
|
943
|
+
height: options.clip.height
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
: options.fullPage ? { origin: "document" } : {}),
|
|
947
|
+
format: {
|
|
948
|
+
type: options.type ?? "png",
|
|
949
|
+
...(options.quality !== undefined ? { quality: options.quality } : {})
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
return Buffer.from(response.data, "base64");
|
|
953
|
+
}
|
|
954
|
+
async pdf(_options = {}) {
|
|
955
|
+
throw new Error("PDF generation is only supported for Headless Chromium");
|
|
956
|
+
}
|
|
957
|
+
viewportSize() {
|
|
958
|
+
return this.currentViewportSize;
|
|
959
|
+
}
|
|
960
|
+
async setViewportSize(viewportSize) {
|
|
961
|
+
await this.client.browsingContextSetViewport({
|
|
962
|
+
context: this.contextId,
|
|
963
|
+
viewport: {
|
|
964
|
+
width: viewportSize.width,
|
|
965
|
+
height: viewportSize.height
|
|
966
|
+
},
|
|
967
|
+
devicePixelRatio: 1
|
|
968
|
+
});
|
|
969
|
+
this.currentViewportSize = viewportSize;
|
|
970
|
+
}
|
|
971
|
+
async dispatchEvent(selector, type, eventInit) {
|
|
972
|
+
await this.runSelectorOperation({
|
|
973
|
+
operation: "dispatchEvent",
|
|
974
|
+
reference: {
|
|
975
|
+
chain: selector,
|
|
976
|
+
pick: { kind: "first" }
|
|
977
|
+
},
|
|
978
|
+
name: type,
|
|
979
|
+
arg: eventInit
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
async requestGC() {
|
|
983
|
+
const response = await this.client.scriptEvaluate({
|
|
984
|
+
expression: "TestUtils.gc()",
|
|
985
|
+
target: {
|
|
986
|
+
context: this.contextId
|
|
987
|
+
},
|
|
988
|
+
awaitPromise: true,
|
|
989
|
+
resultOwnership: "none"
|
|
990
|
+
});
|
|
991
|
+
if (response.type === "exception") {
|
|
992
|
+
throw new Error("Method not implemented.");
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
async textContent(selector) {
|
|
996
|
+
return this.textContentLocator({ chain: selector });
|
|
997
|
+
}
|
|
998
|
+
async innerText(selector) {
|
|
999
|
+
return this.innerTextLocator({ chain: selector });
|
|
1000
|
+
}
|
|
1001
|
+
async innerHTML(selector) {
|
|
1002
|
+
return this.innerHTMLLocator({ chain: selector });
|
|
1003
|
+
}
|
|
1004
|
+
async getAttribute(selector, name) {
|
|
1005
|
+
return this.getAttributeLocator({ chain: selector }, name);
|
|
1006
|
+
}
|
|
1007
|
+
async inputValue(selector) {
|
|
1008
|
+
return this.inputValueLocator({ chain: selector });
|
|
1009
|
+
}
|
|
1010
|
+
async isChecked(selector) {
|
|
1011
|
+
return this.isCheckedLocator({ chain: selector });
|
|
1012
|
+
}
|
|
1013
|
+
async isDisabled(selector) {
|
|
1014
|
+
return this.isDisabledLocator({ chain: selector });
|
|
1015
|
+
}
|
|
1016
|
+
async isEditable(selector) {
|
|
1017
|
+
return this.isEditableLocator({ chain: selector });
|
|
1018
|
+
}
|
|
1019
|
+
async isEnabled(selector) {
|
|
1020
|
+
return this.isEnabledLocator({ chain: selector });
|
|
1021
|
+
}
|
|
1022
|
+
async focus(selector) {
|
|
1023
|
+
await this.focusLocator({ chain: selector });
|
|
1024
|
+
}
|
|
1025
|
+
async setChecked(selector, checked, options) {
|
|
1026
|
+
if (checked) {
|
|
1027
|
+
await this.checkLocator({ chain: selector }, options);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
await this.uncheckLocator({ chain: selector }, options);
|
|
1031
|
+
}
|
|
1032
|
+
async selectOption(selector, values, options) {
|
|
1033
|
+
return this.selectOptionLocator({ chain: selector }, values, options);
|
|
1034
|
+
}
|
|
1035
|
+
async startCSSCoverage(_options) {
|
|
1036
|
+
throw new NotImplementedInProtocolError("bidi", "page.coverage.startCSSCoverage");
|
|
1037
|
+
}
|
|
1038
|
+
async startJSCoverage(_options) {
|
|
1039
|
+
throw new NotImplementedInProtocolError("bidi", "page.coverage.startJSCoverage");
|
|
1040
|
+
}
|
|
1041
|
+
async stopCSSCoverage() {
|
|
1042
|
+
throw new NotImplementedInProtocolError("bidi", "page.coverage.stopCSSCoverage");
|
|
1043
|
+
}
|
|
1044
|
+
async stopJSCoverage() {
|
|
1045
|
+
throw new NotImplementedInProtocolError("bidi", "page.coverage.stopJSCoverage");
|
|
1046
|
+
}
|
|
1047
|
+
async screencastStart() {
|
|
1048
|
+
// Playwright's BiDi backend currently exposes screencast.start/stop as no-op delegates.
|
|
1049
|
+
}
|
|
1050
|
+
async screencastStop() {
|
|
1051
|
+
// Playwright's BiDi backend currently exposes screencast.start/stop as no-op delegates.
|
|
1052
|
+
}
|
|
1053
|
+
async screencastShowActions(options) {
|
|
1054
|
+
this.screencastActionOptions = { ...(options ?? {}) };
|
|
1055
|
+
await this.renderScreencastActions();
|
|
1056
|
+
}
|
|
1057
|
+
async screencastHideActions() {
|
|
1058
|
+
this.resetScreencastActions();
|
|
1059
|
+
await this.renderScreencastActions();
|
|
1060
|
+
}
|
|
1061
|
+
async screencastShowOverlay(options) {
|
|
1062
|
+
const id = `overlay-${++this.screencastOverlayId}`;
|
|
1063
|
+
this.setScreencastOverlay(id, { html: options.html }, options.duration);
|
|
1064
|
+
await this.renderScreencastOverlays();
|
|
1065
|
+
return { id };
|
|
1066
|
+
}
|
|
1067
|
+
async screencastRemoveOverlay(id) {
|
|
1068
|
+
this.clearScreencastOverlay(id);
|
|
1069
|
+
await this.renderScreencastOverlays();
|
|
1070
|
+
}
|
|
1071
|
+
async screencastChapter(options) {
|
|
1072
|
+
const id = `chapter-${++this.screencastOverlayId}`;
|
|
1073
|
+
this.setScreencastOverlay(id, {
|
|
1074
|
+
kind: "chapter",
|
|
1075
|
+
html: createChapterOverlayHtml(options.title, options.description)
|
|
1076
|
+
}, options.duration ?? 2000);
|
|
1077
|
+
await this.renderScreencastOverlays();
|
|
1078
|
+
}
|
|
1079
|
+
async screencastSetOverlayVisible(visible) {
|
|
1080
|
+
this.screencastOverlaysVisible = visible;
|
|
1081
|
+
await this.renderScreencastOverlays();
|
|
1082
|
+
}
|
|
1083
|
+
async keyboardDown(key) {
|
|
1084
|
+
const keyDefinition = keyDescriptionForString(key, this.pressedKeyboardModifiers);
|
|
1085
|
+
await this.client.inputPerformActions({
|
|
1086
|
+
context: this.contextId,
|
|
1087
|
+
actions: [
|
|
1088
|
+
{
|
|
1089
|
+
type: "key",
|
|
1090
|
+
id: "keyboard",
|
|
1091
|
+
actions: [{ type: "keyDown", value: toBiDiKeyValue(resolveSmartModifierString(key)) }]
|
|
1092
|
+
}
|
|
1093
|
+
]
|
|
1094
|
+
});
|
|
1095
|
+
if (isKeyboardModifier(keyDefinition.key)) {
|
|
1096
|
+
this.pressedKeyboardModifiers.add(keyDefinition.key);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
async keyboardInsertText(text) {
|
|
1100
|
+
await this.evaluateFunction(`({ value }) => {
|
|
1101
|
+
const activeElement = document.activeElement;
|
|
1102
|
+
if (
|
|
1103
|
+
activeElement instanceof HTMLInputElement ||
|
|
1104
|
+
activeElement instanceof HTMLTextAreaElement
|
|
1105
|
+
) {
|
|
1106
|
+
const start = activeElement.selectionStart ?? activeElement.value.length;
|
|
1107
|
+
const end = activeElement.selectionEnd ?? activeElement.value.length;
|
|
1108
|
+
activeElement.setRangeText(value, start, end, "end");
|
|
1109
|
+
activeElement.dispatchEvent(new InputEvent("input", {
|
|
1110
|
+
bubbles: true,
|
|
1111
|
+
data: value,
|
|
1112
|
+
inputType: "insertText"
|
|
1113
|
+
}));
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (activeElement instanceof HTMLElement && activeElement.isContentEditable) {
|
|
1118
|
+
document.execCommand("insertText", false, value);
|
|
1119
|
+
}
|
|
1120
|
+
}`, { value: text });
|
|
1121
|
+
}
|
|
1122
|
+
async keyboardPress(key, options) {
|
|
1123
|
+
const tokens = splitKeyboardShortcut(key);
|
|
1124
|
+
const keyName = tokens[tokens.length - 1] ?? "";
|
|
1125
|
+
for (let index = 0; index < tokens.length - 1; index += 1) {
|
|
1126
|
+
await this.keyboardDown(tokens[index] ?? "");
|
|
1127
|
+
}
|
|
1128
|
+
await this.keyboardDown(keyName);
|
|
1129
|
+
if (options?.delay) {
|
|
1130
|
+
await new Promise((resolve) => setTimeout(resolve, options.delay));
|
|
1131
|
+
}
|
|
1132
|
+
await this.keyboardUp(keyName);
|
|
1133
|
+
for (let index = tokens.length - 2; index >= 0; index -= 1) {
|
|
1134
|
+
await this.keyboardUp(tokens[index] ?? "");
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
async keyboardType(text, options) {
|
|
1138
|
+
for (const character of text) {
|
|
1139
|
+
if (isUsKeyboardLayoutKey(character)) {
|
|
1140
|
+
await this.keyboardPress(character, options?.delay === undefined ? undefined : { delay: options.delay });
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
if (options?.delay) {
|
|
1144
|
+
await new Promise((resolve) => setTimeout(resolve, options.delay));
|
|
1145
|
+
}
|
|
1146
|
+
await this.keyboardInsertText(character);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
async keyboardUp(key) {
|
|
1150
|
+
const keyDefinition = keyDescriptionForString(key, this.pressedKeyboardModifiers);
|
|
1151
|
+
await this.client.inputPerformActions({
|
|
1152
|
+
context: this.contextId,
|
|
1153
|
+
actions: [
|
|
1154
|
+
{
|
|
1155
|
+
type: "key",
|
|
1156
|
+
id: "keyboard",
|
|
1157
|
+
actions: [{ type: "keyUp", value: toBiDiKeyValue(resolveSmartModifierString(key)) }]
|
|
1158
|
+
}
|
|
1159
|
+
]
|
|
1160
|
+
});
|
|
1161
|
+
if (isKeyboardModifier(keyDefinition.key)) {
|
|
1162
|
+
this.pressedKeyboardModifiers.delete(keyDefinition.key);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
async mouseClick(x, y, options) {
|
|
1166
|
+
const point = { x, y };
|
|
1167
|
+
await this.performMouseClickActions(point, options);
|
|
1168
|
+
this.currentMousePosition = point;
|
|
1169
|
+
}
|
|
1170
|
+
async mouseDblclick(x, y, options) {
|
|
1171
|
+
await this.mouseClick(x, y, { ...options, clickCount: 2 });
|
|
1172
|
+
}
|
|
1173
|
+
async mouseDown(options) {
|
|
1174
|
+
await this.performMousePointerActions([
|
|
1175
|
+
this.mousePointerDown(options?.button ?? "left")
|
|
1176
|
+
]);
|
|
1177
|
+
}
|
|
1178
|
+
async mouseMove(x, y, options) {
|
|
1179
|
+
const steps = Math.max(options?.steps ?? 1, 1);
|
|
1180
|
+
const start = this.currentMousePosition;
|
|
1181
|
+
const actions = [];
|
|
1182
|
+
for (let index = 1; index <= steps; index += 1) {
|
|
1183
|
+
actions.push(this.mousePointerMove({
|
|
1184
|
+
x: start.x + ((x - start.x) * index) / steps,
|
|
1185
|
+
y: start.y + ((y - start.y) * index) / steps
|
|
1186
|
+
}));
|
|
1187
|
+
}
|
|
1188
|
+
await this.performMousePointerActions(actions);
|
|
1189
|
+
this.currentMousePosition = { x, y };
|
|
1190
|
+
}
|
|
1191
|
+
async mouseUp(options) {
|
|
1192
|
+
await this.performMousePointerActions([
|
|
1193
|
+
this.mousePointerUp(options?.button ?? "left")
|
|
1194
|
+
]);
|
|
1195
|
+
}
|
|
1196
|
+
async mouseWheel(deltaX, deltaY) {
|
|
1197
|
+
await this.evaluateFunction(`({ x, y, deltaX, deltaY, ctrlKey, shiftKey, altKey, metaKey }) => {
|
|
1198
|
+
const target =
|
|
1199
|
+
document.elementFromPoint(x, y) ??
|
|
1200
|
+
document.scrollingElement ??
|
|
1201
|
+
document.documentElement;
|
|
1202
|
+
const event = new WheelEvent("wheel", {
|
|
1203
|
+
bubbles: true,
|
|
1204
|
+
cancelable: true,
|
|
1205
|
+
clientX: x,
|
|
1206
|
+
clientY: y,
|
|
1207
|
+
deltaMode: 0,
|
|
1208
|
+
deltaX,
|
|
1209
|
+
deltaY,
|
|
1210
|
+
ctrlKey,
|
|
1211
|
+
shiftKey,
|
|
1212
|
+
altKey,
|
|
1213
|
+
metaKey
|
|
1214
|
+
});
|
|
1215
|
+
const shouldContinue = target.dispatchEvent(event);
|
|
1216
|
+
if (shouldContinue && !event.defaultPrevented) {
|
|
1217
|
+
globalThis.scrollBy(deltaX, deltaY);
|
|
1218
|
+
}
|
|
1219
|
+
}`, {
|
|
1220
|
+
x: Math.round(this.currentMousePosition.x),
|
|
1221
|
+
y: Math.round(this.currentMousePosition.y),
|
|
1222
|
+
deltaX,
|
|
1223
|
+
deltaY,
|
|
1224
|
+
...keyboardModifierState(this.pressedKeyboardModifiers)
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
async performMousePointerActions(actions) {
|
|
1228
|
+
await this.client.inputPerformActions({
|
|
1229
|
+
context: this.contextId,
|
|
1230
|
+
actions: [
|
|
1231
|
+
{
|
|
1232
|
+
type: "pointer",
|
|
1233
|
+
id: "mouse",
|
|
1234
|
+
parameters: { pointerType: "mouse" },
|
|
1235
|
+
actions
|
|
1236
|
+
}
|
|
1237
|
+
]
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
async performMouseClickActions(point, options, movePointer = true) {
|
|
1241
|
+
if (options?.trial) {
|
|
1242
|
+
if (movePointer) {
|
|
1243
|
+
await this.performMousePointerActions([this.mousePointerMove(point)]);
|
|
1244
|
+
}
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const button = options?.button ?? "left";
|
|
1248
|
+
const clickCount = options?.clickCount ?? 1;
|
|
1249
|
+
const delayMs = options?.delay ?? 0;
|
|
1250
|
+
if (delayMs > 0) {
|
|
1251
|
+
if (movePointer) {
|
|
1252
|
+
await this.performMousePointerActions([this.mousePointerMove(point)]);
|
|
1253
|
+
}
|
|
1254
|
+
for (let index = 1; index <= clickCount; index += 1) {
|
|
1255
|
+
await this.performMousePointerActions([this.mousePointerDown(button)]);
|
|
1256
|
+
await delay(delayMs);
|
|
1257
|
+
await this.performMousePointerActions([this.mousePointerUp(button)]);
|
|
1258
|
+
if (index < clickCount) {
|
|
1259
|
+
await delay(delayMs);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (movePointer) {
|
|
1265
|
+
await this.performMousePointerActions([this.mousePointerMove(point)]);
|
|
1266
|
+
}
|
|
1267
|
+
for (let index = 0; index < clickCount; index += 1) {
|
|
1268
|
+
await this.performMousePointerActions([this.mousePointerDown(button)]);
|
|
1269
|
+
await this.performMousePointerActions([this.mousePointerUp(button)]);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
mousePointerMove(point) {
|
|
1273
|
+
return {
|
|
1274
|
+
type: "pointerMove",
|
|
1275
|
+
x: Math.round(point.x),
|
|
1276
|
+
y: Math.round(point.y),
|
|
1277
|
+
origin: "viewport"
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
mousePointerDown(button) {
|
|
1281
|
+
this.lastMouseButton = button;
|
|
1282
|
+
this.pressedMouseButtons.add(button);
|
|
1283
|
+
return {
|
|
1284
|
+
type: "pointerDown",
|
|
1285
|
+
button: buttonNumber(button)
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
mousePointerUp(button) {
|
|
1289
|
+
this.lastMouseButton = "none";
|
|
1290
|
+
this.pressedMouseButtons.delete(button);
|
|
1291
|
+
return {
|
|
1292
|
+
type: "pointerUp",
|
|
1293
|
+
button: buttonNumber(button)
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
async withTapModifiers(modifiers, action) {
|
|
1297
|
+
const modifiersToRelease = [];
|
|
1298
|
+
for (const modifier of modifiers ?? []) {
|
|
1299
|
+
const normalized = resolveSmartModifierString(modifier);
|
|
1300
|
+
if (!isKeyboardModifier(normalized) || this.pressedKeyboardModifiers.has(normalized)) {
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
await this.keyboardDown(normalized);
|
|
1304
|
+
modifiersToRelease.push(normalized);
|
|
1305
|
+
}
|
|
1306
|
+
try {
|
|
1307
|
+
return await action();
|
|
1308
|
+
}
|
|
1309
|
+
finally {
|
|
1310
|
+
for (let index = modifiersToRelease.length - 1; index >= 0; index -= 1) {
|
|
1311
|
+
await this.keyboardUp(modifiersToRelease[index]);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
async touchscreenTap(x, y) {
|
|
1316
|
+
await this.client.inputPerformActions({
|
|
1317
|
+
context: this.contextId,
|
|
1318
|
+
actions: [
|
|
1319
|
+
{
|
|
1320
|
+
type: "pointer",
|
|
1321
|
+
id: "touchscreen",
|
|
1322
|
+
parameters: { pointerType: "touch" },
|
|
1323
|
+
actions: [
|
|
1324
|
+
{
|
|
1325
|
+
type: "pointerMove",
|
|
1326
|
+
x: Math.round(x),
|
|
1327
|
+
y: Math.round(y),
|
|
1328
|
+
origin: "viewport"
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
type: "pointerDown",
|
|
1332
|
+
button: 0
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
type: "pointerUp",
|
|
1336
|
+
button: 0
|
|
1337
|
+
}
|
|
1338
|
+
]
|
|
1339
|
+
}
|
|
1340
|
+
]
|
|
1341
|
+
});
|
|
1342
|
+
this.currentMousePosition = { x, y };
|
|
1343
|
+
}
|
|
1344
|
+
async tap(selector, options) {
|
|
1345
|
+
const point = await this.resolveActionPoint({ chain: selector }, options, true);
|
|
1346
|
+
await this.withTapModifiers(options?.modifiers, async () => {
|
|
1347
|
+
if (options?.trial) {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
await this.touchscreenTap(point.x, point.y);
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
on(event, listener) {
|
|
1354
|
+
const listeners = this.eventListeners.get(event) ?? new Set();
|
|
1355
|
+
listeners.add(listener);
|
|
1356
|
+
this.eventListeners.set(event, listeners);
|
|
1357
|
+
return () => {
|
|
1358
|
+
const registeredListeners = this.eventListeners.get(event);
|
|
1359
|
+
registeredListeners?.delete(listener);
|
|
1360
|
+
if (registeredListeners?.size === 0) {
|
|
1361
|
+
this.eventListeners.delete(event);
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
async query(selector) {
|
|
1366
|
+
const count = await this.countSelector({
|
|
1367
|
+
chain: selector
|
|
1368
|
+
});
|
|
1369
|
+
if (count === 0) {
|
|
1370
|
+
return null;
|
|
1371
|
+
}
|
|
1372
|
+
return new BidiElementHandleAdapter(this, {
|
|
1373
|
+
chain: selector,
|
|
1374
|
+
pick: { kind: "first" }
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
createHandle(reference) {
|
|
1378
|
+
return new BidiElementHandleAdapter(this, reference);
|
|
1379
|
+
}
|
|
1380
|
+
async queryAll(selector) {
|
|
1381
|
+
const count = await this.countSelector({
|
|
1382
|
+
chain: selector
|
|
1383
|
+
});
|
|
1384
|
+
return Array.from({ length: count }, (_value, index) => {
|
|
1385
|
+
return new BidiElementHandleAdapter(this, {
|
|
1386
|
+
chain: selector,
|
|
1387
|
+
pick: { kind: "nth", index }
|
|
1388
|
+
});
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
async evalOnSelector(selector, expression, isFunction, arg) {
|
|
1392
|
+
return this.evaluateOnReference({
|
|
1393
|
+
chain: selector,
|
|
1394
|
+
pick: { kind: "first" }
|
|
1395
|
+
}, expression, arg, `page.$eval: Failed to find element matching selector "${formatSelectorChain(selector)}"`, isFunction);
|
|
1396
|
+
}
|
|
1397
|
+
async evalOnSelectorAll(selector, expression, isFunction, arg) {
|
|
1398
|
+
return this.evaluateOnReferenceAll({
|
|
1399
|
+
chain: selector
|
|
1400
|
+
}, expression, arg, isFunction);
|
|
1401
|
+
}
|
|
1402
|
+
locator(selector) {
|
|
1403
|
+
return new BidiLocatorAdapter(this, {
|
|
1404
|
+
chain: [selector]
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
getByText(text, options) {
|
|
1408
|
+
return new BidiLocatorAdapter(this, {
|
|
1409
|
+
chain: [createTextLocatorSelector(text, options)]
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
getByAltText(text, options) {
|
|
1413
|
+
return new BidiLocatorAdapter(this, {
|
|
1414
|
+
chain: [createAltTextLocatorSelector(text, options)]
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
getByLabel(text, options) {
|
|
1418
|
+
return new BidiLocatorAdapter(this, {
|
|
1419
|
+
chain: [createLabelLocatorSelector(text, options)]
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
getByPlaceholder(text, options) {
|
|
1423
|
+
return new BidiLocatorAdapter(this, {
|
|
1424
|
+
chain: [createPlaceholderLocatorSelector(text, options)]
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
getByTestId(testId) {
|
|
1428
|
+
return new BidiLocatorAdapter(this, {
|
|
1429
|
+
chain: [createTestIdLocatorSelector(testId)]
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
getByRole(role, options) {
|
|
1433
|
+
return new BidiLocatorAdapter(this, {
|
|
1434
|
+
chain: [createRoleLocatorSelector(role, options)]
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
getByTitle(text, options) {
|
|
1438
|
+
return new BidiLocatorAdapter(this, {
|
|
1439
|
+
chain: [createTitleLocatorSelector(text, options)]
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
async close(options = {}) {
|
|
1443
|
+
if (options.runBeforeUnload) {
|
|
1444
|
+
await this.client.browsingContextClose({
|
|
1445
|
+
context: this.contextId,
|
|
1446
|
+
promptUnload: true
|
|
1447
|
+
});
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
if (this.closed) {
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
this.closeReason = options.reason;
|
|
1454
|
+
this.closed = true;
|
|
1455
|
+
this.resetScreencastActions();
|
|
1456
|
+
for (const overlay of this.screencastOverlays.values()) {
|
|
1457
|
+
if (overlay.removeTimer) {
|
|
1458
|
+
clearTimeout(overlay.removeTimer);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
this.screencastOverlays.clear();
|
|
1462
|
+
this.rejectWaiters(this.createClosedError());
|
|
1463
|
+
this.resolveCloseSignal();
|
|
1464
|
+
await this.client.browsingContextClose({
|
|
1465
|
+
context: this.contextId,
|
|
1466
|
+
promptUnload: false
|
|
1467
|
+
});
|
|
1468
|
+
this.onClosed?.();
|
|
1469
|
+
this.emit("close", undefined);
|
|
1470
|
+
await this.cleanupBiDiListeners();
|
|
1471
|
+
}
|
|
1472
|
+
async bringToFront() {
|
|
1473
|
+
await this.client.browsingContextActivate({
|
|
1474
|
+
context: this.contextId
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
isClosed() {
|
|
1478
|
+
return this.closed;
|
|
1479
|
+
}
|
|
1480
|
+
async applyContextOptions() {
|
|
1481
|
+
if (this.contextOptions.viewport) {
|
|
1482
|
+
await this.setViewportSize(this.contextOptions.viewport);
|
|
1483
|
+
}
|
|
1484
|
+
if (this.contextOptions.locale) {
|
|
1485
|
+
await this.client.emulationSetLocaleOverride({
|
|
1486
|
+
locale: this.contextOptions.locale,
|
|
1487
|
+
contexts: [this.contextId]
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
if (this.contextOptions.timezoneId) {
|
|
1491
|
+
await this.client.emulationSetTimezoneOverride({
|
|
1492
|
+
timezone: this.contextOptions.timezoneId,
|
|
1493
|
+
contexts: [this.contextId]
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
if (this.contextOptions.userAgent) {
|
|
1497
|
+
await this.client.emulationSetUserAgentOverride({
|
|
1498
|
+
userAgent: this.contextOptions.userAgent,
|
|
1499
|
+
contexts: [this.contextId]
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
if (this.contextOptions.extraHTTPHeaders) {
|
|
1503
|
+
await this.updateExtraHTTPHeaders();
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
async updateExtraHTTPHeaders() {
|
|
1507
|
+
const headers = mergeExtraHTTPHeaders(this.contextOptions.extraHTTPHeaders, this.pageExtraHTTPHeaders);
|
|
1508
|
+
await this.client.networkSetExtraHeaders({
|
|
1509
|
+
contexts: [this.contextId],
|
|
1510
|
+
headers: toBiDiOutgoingHeaders(headers)
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
async evaluateExpression(expression) {
|
|
1514
|
+
const response = await this.raceWithClose(this.client.scriptEvaluate({
|
|
1515
|
+
expression: wrapWithSerializedEvaluationResult(expression),
|
|
1516
|
+
target: {
|
|
1517
|
+
context: this.contextId
|
|
1518
|
+
},
|
|
1519
|
+
awaitPromise: true,
|
|
1520
|
+
resultOwnership: "none"
|
|
1521
|
+
}));
|
|
1522
|
+
if (response.type === "exception") {
|
|
1523
|
+
throw new Error(response.exceptionDetails.text || "BiDi evaluation failed.");
|
|
1524
|
+
}
|
|
1525
|
+
return parseSerializedEvaluationResult(extractBiDiValue(response.result));
|
|
1526
|
+
}
|
|
1527
|
+
async evaluateFunction(expression, arg) {
|
|
1528
|
+
const serializedArg = arg === undefined ? "" : serializeForEvaluation(arg);
|
|
1529
|
+
const wrappedExpression = arg === undefined ? `(${expression})()` : `(${expression})(${serializedArg})`;
|
|
1530
|
+
return this.evaluateExpression(wrappedExpression);
|
|
1531
|
+
}
|
|
1532
|
+
async clickLocator(locator, options, retargetForAction) {
|
|
1533
|
+
await this.bringToFront();
|
|
1534
|
+
const point = await this.resolveActionPoint(locator, options, true, retargetForAction);
|
|
1535
|
+
await this.performMousePointerActions([this.mousePointerMove(point)]);
|
|
1536
|
+
await this.resolveActionPoint(locator, options, true, retargetForAction);
|
|
1537
|
+
await this.performMouseClickActions(point, options, false);
|
|
1538
|
+
this.currentMousePosition = point;
|
|
1539
|
+
await this.showScreencastAction("click", point);
|
|
1540
|
+
}
|
|
1541
|
+
async hoverLocator(locator, options) {
|
|
1542
|
+
const point = await this.resolveActionPoint(locator, options);
|
|
1543
|
+
await this.performMousePointerActions([
|
|
1544
|
+
this.mousePointerMove(point)
|
|
1545
|
+
]);
|
|
1546
|
+
this.currentMousePosition = point;
|
|
1547
|
+
}
|
|
1548
|
+
async fillLocator(locator, value, options) {
|
|
1549
|
+
await this.runFillLocatorWithRetry(locator, value, options);
|
|
1550
|
+
try {
|
|
1551
|
+
const point = await this.resolveActionPoint(locator);
|
|
1552
|
+
await this.showScreencastAction("fill", point);
|
|
1553
|
+
}
|
|
1554
|
+
catch { }
|
|
1555
|
+
}
|
|
1556
|
+
async runFillLocatorWithRetry(locator, value, options) {
|
|
1557
|
+
const timeout = options?.timeout ?? 30_000;
|
|
1558
|
+
const deadline = Date.now() + timeout;
|
|
1559
|
+
while (true) {
|
|
1560
|
+
try {
|
|
1561
|
+
await this.runLocatorOperation(locator, {
|
|
1562
|
+
operation: "fill",
|
|
1563
|
+
...(options?.force !== undefined ? { force: options.force } : {}),
|
|
1564
|
+
value
|
|
1565
|
+
});
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
if (options?.force || !shouldRetryFillActionabilityError(error)) {
|
|
1570
|
+
throw error;
|
|
1571
|
+
}
|
|
1572
|
+
if (timeout === 0 || Date.now() + 50 > deadline) {
|
|
1573
|
+
throw error;
|
|
1574
|
+
}
|
|
1575
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
async typeLocator(locator, value, options) {
|
|
1580
|
+
await this.runLocatorOperation(locator, {
|
|
1581
|
+
operation: "focus",
|
|
1582
|
+
resetSelectionIfNotFocused: true
|
|
1583
|
+
});
|
|
1584
|
+
await this.keyboardType(value, options);
|
|
1585
|
+
}
|
|
1586
|
+
async pressLocator(locator, key, options) {
|
|
1587
|
+
await this.runLocatorOperation(locator, {
|
|
1588
|
+
operation: "focus",
|
|
1589
|
+
resetSelectionIfNotFocused: true
|
|
1590
|
+
});
|
|
1591
|
+
await this.keyboardPress(key, options);
|
|
1592
|
+
}
|
|
1593
|
+
async dblclickLocator(locator, options) {
|
|
1594
|
+
await this.clickLocator(locator, { ...options, clickCount: 2 });
|
|
1595
|
+
}
|
|
1596
|
+
async checkLocator(locator, options) {
|
|
1597
|
+
await this.setCheckedLocator(locator, true, options);
|
|
1598
|
+
}
|
|
1599
|
+
async uncheckLocator(locator, options) {
|
|
1600
|
+
await this.setCheckedLocator(locator, false, options);
|
|
1601
|
+
}
|
|
1602
|
+
async setCheckedLocator(locator, checked, options) {
|
|
1603
|
+
const initialState = await this.checkedStateDetailsLocator(locator);
|
|
1604
|
+
if (initialState.matches === checked) {
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
if (!checked && initialState.isRadio) {
|
|
1608
|
+
throw new Error("Cannot uncheck radio button");
|
|
1609
|
+
}
|
|
1610
|
+
await this.clickLocator(locator, options);
|
|
1611
|
+
if (options?.trial) {
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
if (await this.checkedStateLocator(locator) !== checked) {
|
|
1615
|
+
await this.runSelectorOperation({
|
|
1616
|
+
operation: "domClick",
|
|
1617
|
+
reference: {
|
|
1618
|
+
chain: locator.chain,
|
|
1619
|
+
...(locator.pick ? { pick: locator.pick } : {})
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
if (await this.checkedStateLocator(locator) !== checked) {
|
|
1624
|
+
await this.runLocatorOperation(locator, {
|
|
1625
|
+
operation: "check",
|
|
1626
|
+
checked,
|
|
1627
|
+
...(options?.force !== undefined ? { force: options.force } : {})
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
if (await this.checkedStateLocator(locator) !== checked) {
|
|
1631
|
+
throw new Error("Clicking the checkbox did not change its state");
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
async checkedStateLocator(locator) {
|
|
1635
|
+
return this.runLocatorOperation(locator, {
|
|
1636
|
+
operation: "checkedState"
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
async checkedStateDetailsLocator(locator) {
|
|
1640
|
+
return this.runLocatorOperation(locator, {
|
|
1641
|
+
operation: "checkedStateDetails"
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
async focusLocator(locator) {
|
|
1645
|
+
await this.runLocatorOperation(locator, {
|
|
1646
|
+
operation: "focus"
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
async getAttributeLocator(locator, name) {
|
|
1650
|
+
return this.runLocatorOperation(locator, {
|
|
1651
|
+
operation: "getAttribute",
|
|
1652
|
+
name
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
async innerHTMLLocator(locator) {
|
|
1656
|
+
return this.runLocatorOperation(locator, {
|
|
1657
|
+
operation: "innerHTML"
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
async innerTextLocator(locator) {
|
|
1661
|
+
return this.runLocatorOperation(locator, {
|
|
1662
|
+
operation: "innerText"
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
async inputValueLocator(locator) {
|
|
1666
|
+
return this.runLocatorOperation(locator, {
|
|
1667
|
+
operation: "inputValue"
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
async isCheckedLocator(locator) {
|
|
1671
|
+
return this.runLocatorOperation(locator, {
|
|
1672
|
+
operation: "isChecked"
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
async isDisabledLocator(locator) {
|
|
1676
|
+
return this.runLocatorOperation(locator, {
|
|
1677
|
+
operation: "isDisabled"
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
async isEditableLocator(locator) {
|
|
1681
|
+
return this.runLocatorOperation(locator, {
|
|
1682
|
+
operation: "isEditable"
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
async isEnabledLocator(locator) {
|
|
1686
|
+
return this.runLocatorOperation(locator, {
|
|
1687
|
+
operation: "isEnabled"
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
async selectOptionLocator(locator, values, options) {
|
|
1691
|
+
return this.runSelectOptionWithRetry(() => this.runLocatorOperation(locator, {
|
|
1692
|
+
operation: "selectOption",
|
|
1693
|
+
values
|
|
1694
|
+
}), options?.timeout);
|
|
1695
|
+
}
|
|
1696
|
+
async textContentLocator(locator) {
|
|
1697
|
+
return this.runLocatorOperation(locator, {
|
|
1698
|
+
operation: "textContent"
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
async isVisibleLocator(locator) {
|
|
1702
|
+
return this.runLocatorOperation(locator, {
|
|
1703
|
+
operation: "isVisible"
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
async getAttributeReference(reference, name) {
|
|
1707
|
+
return this.runSelectorOperation({
|
|
1708
|
+
operation: "getAttribute",
|
|
1709
|
+
reference,
|
|
1710
|
+
name
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
async innerHTMLReference(reference) {
|
|
1714
|
+
return this.runSelectorOperation({
|
|
1715
|
+
operation: "innerHTML",
|
|
1716
|
+
reference
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
async innerTextReference(reference) {
|
|
1720
|
+
return this.runSelectorOperation({
|
|
1721
|
+
operation: "innerText",
|
|
1722
|
+
reference
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
async inputValueReference(reference) {
|
|
1726
|
+
return this.runSelectorOperation({
|
|
1727
|
+
operation: "inputValue",
|
|
1728
|
+
reference
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
async isCheckedReference(reference) {
|
|
1732
|
+
return this.runSelectorOperation({
|
|
1733
|
+
operation: "isChecked",
|
|
1734
|
+
reference
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
async isDisabledReference(reference) {
|
|
1738
|
+
return this.runSelectorOperation({
|
|
1739
|
+
operation: "isDisabled",
|
|
1740
|
+
reference
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
async isEditableReference(reference) {
|
|
1744
|
+
return this.runSelectorOperation({
|
|
1745
|
+
operation: "isEditable",
|
|
1746
|
+
reference
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
async isEnabledReference(reference) {
|
|
1750
|
+
return this.runSelectorOperation({
|
|
1751
|
+
operation: "isEnabled",
|
|
1752
|
+
reference
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
async focusReference(reference) {
|
|
1756
|
+
await this.runSelectorOperation({
|
|
1757
|
+
operation: "focus",
|
|
1758
|
+
reference
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
async checkReference(reference, checked) {
|
|
1762
|
+
await this.setCheckedReference(reference, checked);
|
|
1763
|
+
}
|
|
1764
|
+
async setCheckedReference(reference, checked, options) {
|
|
1765
|
+
const initialState = await this.checkedStateDetailsReference(reference);
|
|
1766
|
+
if (initialState.matches === checked) {
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
if (!checked && initialState.isRadio) {
|
|
1770
|
+
throw new Error("Cannot uncheck radio button");
|
|
1771
|
+
}
|
|
1772
|
+
await this.clickReference(reference, options);
|
|
1773
|
+
if (options?.trial) {
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
if (await this.checkedStateReference(reference) !== checked) {
|
|
1777
|
+
await this.runSelectorOperation({
|
|
1778
|
+
operation: "domClick",
|
|
1779
|
+
reference
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
if (await this.checkedStateReference(reference) !== checked) {
|
|
1783
|
+
await this.runSelectorOperation({
|
|
1784
|
+
operation: "check",
|
|
1785
|
+
reference,
|
|
1786
|
+
checked,
|
|
1787
|
+
...(options?.force !== undefined ? { force: options.force } : {})
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
if (await this.checkedStateReference(reference) !== checked) {
|
|
1791
|
+
throw new Error("Clicking the checkbox did not change its state");
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
async checkedStateReference(reference) {
|
|
1795
|
+
return this.runSelectorOperation({
|
|
1796
|
+
operation: "checkedState",
|
|
1797
|
+
reference
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
async checkedStateDetailsReference(reference) {
|
|
1801
|
+
return this.runSelectorOperation({
|
|
1802
|
+
operation: "checkedStateDetails",
|
|
1803
|
+
reference
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
async selectOptionReference(reference, values, options) {
|
|
1807
|
+
return this.runSelectOptionWithRetry(() => this.runSelectorOperation({
|
|
1808
|
+
operation: "selectOption",
|
|
1809
|
+
reference,
|
|
1810
|
+
values
|
|
1811
|
+
}), options?.timeout);
|
|
1812
|
+
}
|
|
1813
|
+
async resolveActionPoint(locator, options, waitForEnabled, retargetForAction) {
|
|
1814
|
+
return this.runSelectorOperation({
|
|
1815
|
+
operation: "actionPoint",
|
|
1816
|
+
reference: {
|
|
1817
|
+
chain: locator.chain,
|
|
1818
|
+
...(locator.pick ? { pick: locator.pick } : {})
|
|
1819
|
+
},
|
|
1820
|
+
...(options?.force !== undefined ? { force: options.force } : {}),
|
|
1821
|
+
...(options?.position ? { position: options.position } : {}),
|
|
1822
|
+
...(waitForEnabled ? { waitForEnabled } : {}),
|
|
1823
|
+
...(retargetForAction ? { retargetForAction } : {})
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
async runLocatorOperation(locator, payload) {
|
|
1827
|
+
return this.runSelectorOperation({
|
|
1828
|
+
...payload,
|
|
1829
|
+
reference: {
|
|
1830
|
+
chain: locator.chain,
|
|
1831
|
+
...(locator.pick ? { pick: locator.pick } : {})
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
async runSelectOptionWithRetry(action, timeout) {
|
|
1836
|
+
const effectiveTimeout = timeout ?? 30_000;
|
|
1837
|
+
const deadline = Date.now() + effectiveTimeout;
|
|
1838
|
+
while (true) {
|
|
1839
|
+
const result = await action();
|
|
1840
|
+
if (!isSelectOptionRetryResult(result)) {
|
|
1841
|
+
return result;
|
|
1842
|
+
}
|
|
1843
|
+
if (effectiveTimeout === 0 || Date.now() + 50 > deadline) {
|
|
1844
|
+
throw new TimeoutError(`page.selectOption: Timeout ${effectiveTimeout}ms exceeded.`);
|
|
1845
|
+
}
|
|
1846
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
async countSelector(reference) {
|
|
1850
|
+
return this.runSelectorOperation({
|
|
1851
|
+
operation: "count",
|
|
1852
|
+
reference
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
async boundingBoxReference(reference) {
|
|
1856
|
+
return this.runSelectorOperation({
|
|
1857
|
+
operation: "boundingBox",
|
|
1858
|
+
reference
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
async evaluateOnReference(reference, expression, arg, missingMessage, isFunction) {
|
|
1862
|
+
return this.runSelectorOperation({
|
|
1863
|
+
operation: "evaluate",
|
|
1864
|
+
reference,
|
|
1865
|
+
expression,
|
|
1866
|
+
arg,
|
|
1867
|
+
...(isFunction !== undefined ? { isFunction } : {}),
|
|
1868
|
+
...(missingMessage ? { missingMessage } : {})
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
async createHandleReference(reference, missingMessage) {
|
|
1872
|
+
const result = await this.runSelectorOperation({
|
|
1873
|
+
operation: "createHandle",
|
|
1874
|
+
reference,
|
|
1875
|
+
...(missingMessage ? { missingMessage } : {})
|
|
1876
|
+
});
|
|
1877
|
+
return {
|
|
1878
|
+
chain: [],
|
|
1879
|
+
handleId: result.handleId
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
async evaluateOnReferenceAll(reference, expression, arg, isFunction) {
|
|
1883
|
+
return this.runSelectorOperation({
|
|
1884
|
+
operation: "evaluateAll",
|
|
1885
|
+
reference,
|
|
1886
|
+
expression,
|
|
1887
|
+
arg,
|
|
1888
|
+
...(isFunction !== undefined ? { isFunction } : {})
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
async elementTextContent(reference) {
|
|
1892
|
+
return this.runSelectorOperation({
|
|
1893
|
+
operation: "textContent",
|
|
1894
|
+
reference
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
async elementIsVisible(reference) {
|
|
1898
|
+
return this.runSelectorOperation({
|
|
1899
|
+
operation: "isVisible",
|
|
1900
|
+
reference
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
async clickReference(reference, options) {
|
|
1904
|
+
await this.bringToFront();
|
|
1905
|
+
const point = await this.runSelectorOperation({
|
|
1906
|
+
operation: "actionPoint",
|
|
1907
|
+
reference,
|
|
1908
|
+
...(options?.force !== undefined ? { force: options.force } : {}),
|
|
1909
|
+
...(options?.position ? { position: options.position } : {}),
|
|
1910
|
+
waitForEnabled: true
|
|
1911
|
+
});
|
|
1912
|
+
await this.performMousePointerActions([this.mousePointerMove(point)]);
|
|
1913
|
+
await this.runSelectorOperation({
|
|
1914
|
+
operation: "actionPoint",
|
|
1915
|
+
reference,
|
|
1916
|
+
...(options?.force !== undefined ? { force: options.force } : {}),
|
|
1917
|
+
...(options?.position ? { position: options.position } : {}),
|
|
1918
|
+
waitForEnabled: true
|
|
1919
|
+
});
|
|
1920
|
+
await this.performMouseClickActions(point, options, false);
|
|
1921
|
+
this.currentMousePosition = point;
|
|
1922
|
+
await this.showScreencastAction("click", point);
|
|
1923
|
+
}
|
|
1924
|
+
async tapReference(reference, options) {
|
|
1925
|
+
const point = await this.runSelectorOperation({
|
|
1926
|
+
operation: "actionPoint",
|
|
1927
|
+
reference,
|
|
1928
|
+
...(options?.force !== undefined ? { force: options.force } : {}),
|
|
1929
|
+
...(options?.position ? { position: options.position } : {}),
|
|
1930
|
+
waitForEnabled: true
|
|
1931
|
+
});
|
|
1932
|
+
await this.withTapModifiers(options?.modifiers, async () => {
|
|
1933
|
+
if (options?.trial) {
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
await this.touchscreenTap(point.x, point.y);
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
async hoverReference(reference, options) {
|
|
1940
|
+
const point = await this.runSelectorOperation({
|
|
1941
|
+
operation: "actionPoint",
|
|
1942
|
+
reference,
|
|
1943
|
+
...(options?.force !== undefined ? { force: options.force } : {}),
|
|
1944
|
+
...(options?.position ? { position: options.position } : {})
|
|
1945
|
+
});
|
|
1946
|
+
await this.performMousePointerActions([
|
|
1947
|
+
this.mousePointerMove(point)
|
|
1948
|
+
]);
|
|
1949
|
+
this.currentMousePosition = point;
|
|
1950
|
+
}
|
|
1951
|
+
async fillReference(reference, value, options) {
|
|
1952
|
+
await this.runSelectorOperation({
|
|
1953
|
+
operation: "fill",
|
|
1954
|
+
reference,
|
|
1955
|
+
value,
|
|
1956
|
+
...(options?.force !== undefined ? { force: options.force } : {}),
|
|
1957
|
+
timeoutMs: options?.timeout ?? 30_000
|
|
1958
|
+
});
|
|
1959
|
+
try {
|
|
1960
|
+
const point = await this.runSelectorOperation({
|
|
1961
|
+
operation: "actionPoint",
|
|
1962
|
+
reference
|
|
1963
|
+
});
|
|
1964
|
+
await this.showScreencastAction("fill", point);
|
|
1965
|
+
}
|
|
1966
|
+
catch { }
|
|
1967
|
+
}
|
|
1968
|
+
async typeReference(reference, value, options) {
|
|
1969
|
+
await this.runSelectorOperation({
|
|
1970
|
+
operation: "focus",
|
|
1971
|
+
reference,
|
|
1972
|
+
resetSelectionIfNotFocused: true
|
|
1973
|
+
});
|
|
1974
|
+
await this.keyboardType(value, options);
|
|
1975
|
+
}
|
|
1976
|
+
async pressReference(reference, key, options) {
|
|
1977
|
+
await this.runSelectorOperation({
|
|
1978
|
+
operation: "focus",
|
|
1979
|
+
reference,
|
|
1980
|
+
resetSelectionIfNotFocused: true
|
|
1981
|
+
});
|
|
1982
|
+
await this.keyboardPress(key, options);
|
|
1983
|
+
}
|
|
1984
|
+
async runSelectorOperation(payload) {
|
|
1985
|
+
return this.evaluateFunction(SELECTOR_RUNTIME_SOURCE, payload);
|
|
1986
|
+
}
|
|
1987
|
+
async frameSnapshots() {
|
|
1988
|
+
return [{
|
|
1989
|
+
id: "main",
|
|
1990
|
+
name: "",
|
|
1991
|
+
ownerElementChain: [],
|
|
1992
|
+
parentId: null,
|
|
1993
|
+
referenceChain: [],
|
|
1994
|
+
url: this.currentUrl
|
|
1995
|
+
}];
|
|
1996
|
+
}
|
|
1997
|
+
setScreencastOverlay(id, overlay, duration) {
|
|
1998
|
+
this.clearScreencastOverlay(id);
|
|
1999
|
+
const state = { ...overlay };
|
|
2000
|
+
if (duration !== undefined) {
|
|
2001
|
+
state.removeTimer = setTimeout(() => {
|
|
2002
|
+
this.clearScreencastOverlay(id);
|
|
2003
|
+
void this.renderScreencastOverlays();
|
|
2004
|
+
}, duration);
|
|
2005
|
+
}
|
|
2006
|
+
this.screencastOverlays.set(id, state);
|
|
2007
|
+
}
|
|
2008
|
+
clearScreencastOverlay(id) {
|
|
2009
|
+
const existing = this.screencastOverlays.get(id);
|
|
2010
|
+
if (existing?.removeTimer) {
|
|
2011
|
+
clearTimeout(existing.removeTimer);
|
|
2012
|
+
}
|
|
2013
|
+
this.screencastOverlays.delete(id);
|
|
2014
|
+
}
|
|
2015
|
+
async renderScreencastOverlays() {
|
|
2016
|
+
if (this.closed) {
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
await this.evaluateFunction(RENDER_SCREencast_OVERLAYS_SOURCE, {
|
|
2020
|
+
visible: this.screencastOverlaysVisible,
|
|
2021
|
+
overlays: Array.from(this.screencastOverlays.entries()).map(([id, overlay]) => ({
|
|
2022
|
+
id,
|
|
2023
|
+
html: overlay.html,
|
|
2024
|
+
...(overlay.kind ? { kind: overlay.kind } : {})
|
|
2025
|
+
}))
|
|
2026
|
+
}).catch(() => { });
|
|
2027
|
+
}
|
|
2028
|
+
resetScreencastActions() {
|
|
2029
|
+
this.screencastActionAbortController?.abort();
|
|
2030
|
+
this.screencastActionAbortController = null;
|
|
2031
|
+
this.screencastActionAnnotation = null;
|
|
2032
|
+
this.screencastActionOptions = null;
|
|
2033
|
+
}
|
|
2034
|
+
async showScreencastAction(title, point) {
|
|
2035
|
+
if (!this.screencastActionOptions || this.closed) {
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
const abortController = new AbortController();
|
|
2039
|
+
this.screencastActionAbortController?.abort();
|
|
2040
|
+
this.screencastActionAbortController = abortController;
|
|
2041
|
+
this.screencastActionAnnotation = {
|
|
2042
|
+
title,
|
|
2043
|
+
point,
|
|
2044
|
+
...(this.screencastActionOptions.cursor !== "none" ? { cursorPoint: point } : {}),
|
|
2045
|
+
highlightBox: createScreencastHighlightBox(point)
|
|
2046
|
+
};
|
|
2047
|
+
await this.renderScreencastActions();
|
|
2048
|
+
const completed = await waitForAbortableTimeout(this.screencastActionOptions.duration ?? 500, abortController.signal);
|
|
2049
|
+
if (!completed || this.screencastActionAbortController !== abortController) {
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
this.screencastActionAbortController = null;
|
|
2053
|
+
this.screencastActionAnnotation = null;
|
|
2054
|
+
await this.renderScreencastActions();
|
|
2055
|
+
}
|
|
2056
|
+
async renderScreencastActions() {
|
|
2057
|
+
if (this.closed) {
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
await this.evaluateFunction(RENDER_SCREENCAST_ACTIONS_SOURCE, {
|
|
2061
|
+
enabled: Boolean(this.screencastActionOptions),
|
|
2062
|
+
annotation: this.screencastActionOptions && this.screencastActionAnnotation
|
|
2063
|
+
? {
|
|
2064
|
+
title: this.screencastActionAnnotation.title,
|
|
2065
|
+
point: this.screencastActionAnnotation.point,
|
|
2066
|
+
...(this.screencastActionAnnotation.cursorPoint
|
|
2067
|
+
? { cursorPoint: this.screencastActionAnnotation.cursorPoint }
|
|
2068
|
+
: {}),
|
|
2069
|
+
...(this.screencastActionAnnotation.highlightBox
|
|
2070
|
+
? { highlightBox: this.screencastActionAnnotation.highlightBox }
|
|
2071
|
+
: {}),
|
|
2072
|
+
...(this.screencastActionOptions.position
|
|
2073
|
+
? { position: this.screencastActionOptions.position }
|
|
2074
|
+
: {}),
|
|
2075
|
+
...(this.screencastActionOptions.fontSize !== undefined
|
|
2076
|
+
? { fontSize: this.screencastActionOptions.fontSize }
|
|
2077
|
+
: {}),
|
|
2078
|
+
...(this.screencastActionOptions.cursor
|
|
2079
|
+
? { cursor: this.screencastActionOptions.cursor }
|
|
2080
|
+
: {})
|
|
2081
|
+
}
|
|
2082
|
+
: null
|
|
2083
|
+
}).catch(() => { });
|
|
2084
|
+
}
|
|
2085
|
+
attachBiDiListeners() {
|
|
2086
|
+
this.attachBiDiListener("browsingContext.domContentLoaded", (payload) => {
|
|
2087
|
+
if (!hasContext(payload, this.contextId)) {
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
this.currentUrl = extractBiDiContextUrl(payload) ?? this.currentUrl;
|
|
2091
|
+
this.domContentLoaded = true;
|
|
2092
|
+
this.flushWaiters();
|
|
2093
|
+
this.emit("domcontentloaded", undefined);
|
|
2094
|
+
void this.renderScreencastActions();
|
|
2095
|
+
void this.renderScreencastOverlays();
|
|
2096
|
+
});
|
|
2097
|
+
this.attachBiDiListener("browsingContext.fragmentNavigated", (payload) => {
|
|
2098
|
+
if (!hasContext(payload, this.contextId)) {
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
this.currentUrl = extractBiDiContextUrl(payload) ?? this.currentUrl;
|
|
2102
|
+
this.sameDocumentNavigation = true;
|
|
2103
|
+
this.domContentLoaded = true;
|
|
2104
|
+
this.loadFired = true;
|
|
2105
|
+
void this.renderScreencastActions();
|
|
2106
|
+
void this.renderScreencastOverlays();
|
|
2107
|
+
if (this.allowSameDocumentNavigationToResolveWaiters) {
|
|
2108
|
+
this.flushWaiters();
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
this.attachBiDiListener("browsingContext.historyUpdated", (payload) => {
|
|
2112
|
+
if (!hasContext(payload, this.contextId)) {
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
this.currentUrl = extractBiDiContextUrl(payload) ?? this.currentUrl;
|
|
2116
|
+
this.sameDocumentNavigation = true;
|
|
2117
|
+
this.domContentLoaded = true;
|
|
2118
|
+
this.loadFired = true;
|
|
2119
|
+
void this.renderScreencastActions();
|
|
2120
|
+
void this.renderScreencastOverlays();
|
|
2121
|
+
if (this.allowSameDocumentNavigationToResolveWaiters) {
|
|
2122
|
+
this.flushWaiters();
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
this.attachBiDiListener("browsingContext.load", (payload) => {
|
|
2126
|
+
if (!hasContext(payload, this.contextId)) {
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
this.currentUrl = extractBiDiContextUrl(payload) ?? this.currentUrl;
|
|
2130
|
+
this.loadFired = true;
|
|
2131
|
+
this.flushWaiters();
|
|
2132
|
+
this.emit("load", undefined);
|
|
2133
|
+
void this.renderScreencastActions();
|
|
2134
|
+
void this.renderScreencastOverlays();
|
|
2135
|
+
});
|
|
2136
|
+
this.attachBiDiListener("browsingContext.userPromptOpened", (payload) => {
|
|
2137
|
+
if (!hasContext(payload, this.contextId)) {
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
const prompt = payload;
|
|
2141
|
+
this.emit("dialog", this.createDialogPayload({
|
|
2142
|
+
defaultValue: prompt.defaultValue ?? "",
|
|
2143
|
+
message: prompt.message ?? "",
|
|
2144
|
+
type: prompt.type ?? "alert"
|
|
2145
|
+
}));
|
|
2146
|
+
});
|
|
2147
|
+
this.attachBiDiListener("log.entryAdded", (payload) => {
|
|
2148
|
+
if (!hasLogContext(payload, this.contextId)) {
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
const logPayload = payload;
|
|
2152
|
+
this.emit("console", {
|
|
2153
|
+
args: () => [],
|
|
2154
|
+
location: () => ({
|
|
2155
|
+
column: 0,
|
|
2156
|
+
columnNumber: 0,
|
|
2157
|
+
line: 0,
|
|
2158
|
+
lineNumber: 0,
|
|
2159
|
+
url: ""
|
|
2160
|
+
}),
|
|
2161
|
+
page: () => null,
|
|
2162
|
+
text: () => logPayload.text ?? "",
|
|
2163
|
+
timestamp: () => Date.now(),
|
|
2164
|
+
type: () => normalizeConsoleMessageType(logPayload.method ?? logPayload.type ?? "log"),
|
|
2165
|
+
worker: () => null
|
|
2166
|
+
});
|
|
2167
|
+
});
|
|
2168
|
+
this.attachBiDiListener("network.beforeRequestSent", (payload) => {
|
|
2169
|
+
if (!hasContext(payload, this.contextId)) {
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
const requestPayload = payload;
|
|
2173
|
+
this.emit("request", {
|
|
2174
|
+
...(requestPayload.context ? { frameId: requestPayload.context } : {}),
|
|
2175
|
+
headers: mapBiDiHeaders(requestPayload.request.headers),
|
|
2176
|
+
...(requestPayload.request.destination
|
|
2177
|
+
? { isNavigationRequest: requestPayload.request.destination === "document" && requestPayload.navigation !== null }
|
|
2178
|
+
: {}),
|
|
2179
|
+
method: requestPayload.request.method,
|
|
2180
|
+
...(requestPayload.request.request ? { requestId: requestPayload.request.request } : {}),
|
|
2181
|
+
...(requestPayload.request.destination
|
|
2182
|
+
? { resourceType: requestPayload.request.destination.toLowerCase() }
|
|
2183
|
+
: {}),
|
|
2184
|
+
url: requestPayload.request.url
|
|
2185
|
+
});
|
|
2186
|
+
});
|
|
2187
|
+
this.attachBiDiListener("network.responseStarted", (payload) => {
|
|
2188
|
+
if (!hasContext(payload, this.contextId)) {
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
const responsePayload = payload;
|
|
2192
|
+
const response = createPageResponse({
|
|
2193
|
+
fromCache: responsePayload.response.fromCache,
|
|
2194
|
+
...(responsePayload.context ? { frameId: responsePayload.context } : {}),
|
|
2195
|
+
headers: mapBiDiHeaders(responsePayload.response.headers),
|
|
2196
|
+
isNavigationRequest: responsePayload.request.destination === "document" &&
|
|
2197
|
+
responsePayload.navigation !== null,
|
|
2198
|
+
mimeType: responsePayload.response.mimeType,
|
|
2199
|
+
requestId: responsePayload.request.request,
|
|
2200
|
+
resourceType: responsePayload.request.destination.toLowerCase(),
|
|
2201
|
+
status: responsePayload.response.status,
|
|
2202
|
+
statusText: responsePayload.response.statusText,
|
|
2203
|
+
text: () => this.getResponseText(responsePayload.request.request),
|
|
2204
|
+
url: responsePayload.response.url || responsePayload.request.url
|
|
2205
|
+
});
|
|
2206
|
+
if (responsePayload.context === this.contextId &&
|
|
2207
|
+
responsePayload.navigation !== null &&
|
|
2208
|
+
responsePayload.request.destination === "document") {
|
|
2209
|
+
this.currentUrl = response.url;
|
|
2210
|
+
}
|
|
2211
|
+
this.emit("response", response);
|
|
2212
|
+
if (this.navigationResponseCapture &&
|
|
2213
|
+
responsePayload.context === this.contextId &&
|
|
2214
|
+
responsePayload.navigation !== null &&
|
|
2215
|
+
responsePayload.request.destination === "document" &&
|
|
2216
|
+
shouldCaptureNavigationResponseUrl(response.url)) {
|
|
2217
|
+
this.navigationResponseCapture.lastResponse = response;
|
|
2218
|
+
}
|
|
2219
|
+
});
|
|
2220
|
+
this.attachBiDiListener("network.responseCompleted", (payload) => {
|
|
2221
|
+
if (!hasContext(payload, this.contextId)) {
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
const completedPayload = payload;
|
|
2225
|
+
if (completedPayload.context === this.contextId &&
|
|
2226
|
+
completedPayload.navigation !== null &&
|
|
2227
|
+
completedPayload.request.destination === "document") {
|
|
2228
|
+
this.currentUrl = completedPayload.request.url;
|
|
2229
|
+
}
|
|
2230
|
+
this.emit("requestfinished", {
|
|
2231
|
+
...(completedPayload.context ? { frameId: completedPayload.context } : {}),
|
|
2232
|
+
headers: [],
|
|
2233
|
+
isNavigationRequest: completedPayload.request.destination === "document" &&
|
|
2234
|
+
completedPayload.navigation !== null,
|
|
2235
|
+
method: completedPayload.request.method ?? "GET",
|
|
2236
|
+
requestId: completedPayload.request.request,
|
|
2237
|
+
resourceType: completedPayload.request.destination.toLowerCase(),
|
|
2238
|
+
url: completedPayload.request.url
|
|
2239
|
+
});
|
|
2240
|
+
});
|
|
2241
|
+
this.attachBiDiListener("network.fetchError", (payload) => {
|
|
2242
|
+
if (!hasContext(payload, this.contextId)) {
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
const failedPayload = payload;
|
|
2246
|
+
this.emit("requestfailed", {
|
|
2247
|
+
errorText: failedPayload.errorText,
|
|
2248
|
+
...(failedPayload.request.destination
|
|
2249
|
+
? { isNavigationRequest: failedPayload.request.destination === "document" }
|
|
2250
|
+
: {}),
|
|
2251
|
+
method: failedPayload.request.method,
|
|
2252
|
+
...(failedPayload.request.request ? { requestId: failedPayload.request.request } : {}),
|
|
2253
|
+
...(failedPayload.request.destination
|
|
2254
|
+
? { resourceType: failedPayload.request.destination.toLowerCase() }
|
|
2255
|
+
: {}),
|
|
2256
|
+
url: failedPayload.request.url
|
|
2257
|
+
});
|
|
2258
|
+
});
|
|
2259
|
+
this.attachBiDiListener("browsingContext.contextDestroyed", (payload) => {
|
|
2260
|
+
if (!hasContext(payload, this.contextId) || this.closed) {
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
this.closed = true;
|
|
2264
|
+
this.rejectWaiters(this.createClosedError());
|
|
2265
|
+
this.resolveCloseSignal();
|
|
2266
|
+
this.onClosed?.();
|
|
2267
|
+
this.emit("close", undefined);
|
|
2268
|
+
void this.cleanupBiDiListeners();
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
attachBiDiListener(event, listener) {
|
|
2272
|
+
this.bidiListeners.set(event, listener);
|
|
2273
|
+
this.client.on(event, listener);
|
|
2274
|
+
}
|
|
2275
|
+
async cleanupBiDiListeners() {
|
|
2276
|
+
for (const [event, listener] of this.bidiListeners) {
|
|
2277
|
+
this.client.removeListener(event, listener);
|
|
2278
|
+
}
|
|
2279
|
+
this.bidiListeners.clear();
|
|
2280
|
+
try {
|
|
2281
|
+
await this.client.sessionUnsubscribe({
|
|
2282
|
+
contexts: [this.contextId],
|
|
2283
|
+
events: [
|
|
2284
|
+
"browsingContext.contextDestroyed",
|
|
2285
|
+
"browsingContext.domContentLoaded",
|
|
2286
|
+
"browsingContext.fragmentNavigated",
|
|
2287
|
+
"browsingContext.historyUpdated",
|
|
2288
|
+
"browsingContext.load",
|
|
2289
|
+
"log.entryAdded",
|
|
2290
|
+
"network.beforeRequestSent",
|
|
2291
|
+
"network.responseCompleted",
|
|
2292
|
+
"network.fetchError",
|
|
2293
|
+
"network.responseStarted"
|
|
2294
|
+
]
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
catch { }
|
|
2298
|
+
if (this.responseDataCollector) {
|
|
2299
|
+
try {
|
|
2300
|
+
await this.client.networkRemoveDataCollector({
|
|
2301
|
+
collector: this.responseDataCollector
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
catch { }
|
|
2305
|
+
this.responseDataCollector = undefined;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
emit(event, payload) {
|
|
2309
|
+
const listeners = this.eventListeners.get(event);
|
|
2310
|
+
if (!listeners) {
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
for (const listener of Array.from(listeners)) {
|
|
2314
|
+
if (payload === undefined) {
|
|
2315
|
+
listener();
|
|
2316
|
+
continue;
|
|
2317
|
+
}
|
|
2318
|
+
listener(payload);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
createClosedError() {
|
|
2322
|
+
return new Error(this.closeReason ?? "Target page, context or browser has been closed");
|
|
2323
|
+
}
|
|
2324
|
+
raceWithClose(promise) {
|
|
2325
|
+
if (this.closed) {
|
|
2326
|
+
return Promise.reject(this.createClosedError());
|
|
2327
|
+
}
|
|
2328
|
+
return Promise.race([
|
|
2329
|
+
promise,
|
|
2330
|
+
this.closePromise.then(() => {
|
|
2331
|
+
throw this.createClosedError();
|
|
2332
|
+
})
|
|
2333
|
+
]);
|
|
2334
|
+
}
|
|
2335
|
+
resolveCloseSignal() {
|
|
2336
|
+
if (this.closePromiseResolved) {
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
this.closePromiseResolved = true;
|
|
2340
|
+
this.resolveClosePromise();
|
|
2341
|
+
}
|
|
2342
|
+
createDialogPayload(input) {
|
|
2343
|
+
let handled = false;
|
|
2344
|
+
const respond = async (accept, promptText) => {
|
|
2345
|
+
if (handled) {
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
handled = true;
|
|
2349
|
+
await this.client.browsingContextHandleUserPrompt({
|
|
2350
|
+
context: this.contextId,
|
|
2351
|
+
accept,
|
|
2352
|
+
...(promptText !== undefined ? { userText: promptText } : {})
|
|
2353
|
+
});
|
|
2354
|
+
};
|
|
2355
|
+
const dialog = {
|
|
2356
|
+
accept: (promptText) => respond(true, promptText),
|
|
2357
|
+
defaultValue: () => input.defaultValue,
|
|
2358
|
+
dismiss: () => respond(false),
|
|
2359
|
+
message: () => input.message,
|
|
2360
|
+
page: () => input.page?.() ?? null,
|
|
2361
|
+
type: () => input.type
|
|
2362
|
+
};
|
|
2363
|
+
if ((this.eventListeners.get("dialog")?.size ?? 0) === 0) {
|
|
2364
|
+
void dialog.dismiss().catch(() => { });
|
|
2365
|
+
}
|
|
2366
|
+
return dialog;
|
|
2367
|
+
}
|
|
2368
|
+
async navigateHistory(delta, options) {
|
|
2369
|
+
const previousUrl = this.url();
|
|
2370
|
+
const capture = this.beginNavigationResponseCapture();
|
|
2371
|
+
this.resetNavigationState();
|
|
2372
|
+
this.allowSameDocumentNavigationToResolveWaiters = true;
|
|
2373
|
+
try {
|
|
2374
|
+
await this.client.browsingContextTraverseHistory({
|
|
2375
|
+
context: this.contextId,
|
|
2376
|
+
delta
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
catch (error) {
|
|
2380
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2381
|
+
if (/history/i.test(message) || /no such/i.test(message)) {
|
|
2382
|
+
return null;
|
|
2383
|
+
}
|
|
2384
|
+
throw error;
|
|
2385
|
+
}
|
|
2386
|
+
const waitUntil = verifyLifecycle("waitUntil", options.waitUntil ?? "load");
|
|
2387
|
+
if (waitUntil !== "commit") {
|
|
2388
|
+
const fallbackState = { cancelled: false };
|
|
2389
|
+
try {
|
|
2390
|
+
await Promise.race([
|
|
2391
|
+
this.waitForPendingLoadState(waitUntil, options.timeout, previousUrl),
|
|
2392
|
+
this.waitForUrlChangeWithReadyState(waitUntil, previousUrl, options.timeout ?? 30_000, fallbackState)
|
|
2393
|
+
]);
|
|
2394
|
+
}
|
|
2395
|
+
finally {
|
|
2396
|
+
fallbackState.cancelled = true;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
const currentUrl = this.url();
|
|
2400
|
+
if (this.navigationResponseCapture === capture) {
|
|
2401
|
+
this.navigationResponseCapture = undefined;
|
|
2402
|
+
}
|
|
2403
|
+
if (currentUrl === previousUrl) {
|
|
2404
|
+
return null;
|
|
2405
|
+
}
|
|
2406
|
+
if (capture.lastResponse) {
|
|
2407
|
+
return capture.lastResponse;
|
|
2408
|
+
}
|
|
2409
|
+
if (this.sameDocumentNavigation) {
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
return createPageResponse({
|
|
2413
|
+
fromCache: false,
|
|
2414
|
+
headers: [],
|
|
2415
|
+
mimeType: "text/html",
|
|
2416
|
+
status: 200,
|
|
2417
|
+
statusText: "OK",
|
|
2418
|
+
text: async () => "",
|
|
2419
|
+
url: currentUrl
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
isStateSatisfied(state) {
|
|
2423
|
+
if (this.sameDocumentNavigation && this.allowSameDocumentNavigationToResolveWaiters) {
|
|
2424
|
+
return true;
|
|
2425
|
+
}
|
|
2426
|
+
switch (state) {
|
|
2427
|
+
case "domcontentloaded":
|
|
2428
|
+
return this.domContentLoaded;
|
|
2429
|
+
case "load":
|
|
2430
|
+
case "networkidle":
|
|
2431
|
+
return this.loadFired;
|
|
2432
|
+
case "commit":
|
|
2433
|
+
return true;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
async isCurrentDocumentReadyFor(state) {
|
|
2437
|
+
try {
|
|
2438
|
+
const readyState = await this.evaluateExpression("document.readyState");
|
|
2439
|
+
if (state === "domcontentloaded") {
|
|
2440
|
+
return readyState === "interactive" || readyState === "complete";
|
|
2441
|
+
}
|
|
2442
|
+
return readyState === "complete";
|
|
2443
|
+
}
|
|
2444
|
+
catch {
|
|
2445
|
+
return false;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
flushWaiters() {
|
|
2449
|
+
for (const waiter of Array.from(this.stateWaiters)) {
|
|
2450
|
+
if (waiter.previousUrl !== undefined && this.currentUrl === waiter.previousUrl) {
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
if (this.isStateSatisfied(waiter.state)) {
|
|
2454
|
+
waiter.resolve();
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
rejectWaiters(error) {
|
|
2459
|
+
for (const waiter of Array.from(this.stateWaiters)) {
|
|
2460
|
+
waiter.reject(error);
|
|
2461
|
+
}
|
|
2462
|
+
this.stateWaiters.clear();
|
|
2463
|
+
}
|
|
2464
|
+
resetNavigationState() {
|
|
2465
|
+
this.domContentLoaded = false;
|
|
2466
|
+
this.loadFired = false;
|
|
2467
|
+
this.sameDocumentNavigation = false;
|
|
2468
|
+
this.allowSameDocumentNavigationToResolveWaiters = false;
|
|
2469
|
+
}
|
|
2470
|
+
async waitForUrlChangeWithReadyState(state, previousUrl, timeout, cancelSignal) {
|
|
2471
|
+
const deadline = timeout === 0 ? Number.POSITIVE_INFINITY : Date.now() + timeout;
|
|
2472
|
+
while (this.currentUrl === previousUrl) {
|
|
2473
|
+
if (cancelSignal?.cancelled || this.closed) {
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
if (Date.now() > deadline) {
|
|
2477
|
+
throw new TimeoutError(`page.waitForLoadState: Timeout ${timeout}ms exceeded.`);
|
|
2478
|
+
}
|
|
2479
|
+
await delay(50);
|
|
2480
|
+
}
|
|
2481
|
+
if (state === "commit") {
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
while (true) {
|
|
2485
|
+
if (cancelSignal?.cancelled || this.closed) {
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
if (this.isStateSatisfied(state)) {
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
if (await this.isCurrentDocumentReadyFor(state)) {
|
|
2492
|
+
if (state === "domcontentloaded") {
|
|
2493
|
+
this.domContentLoaded = true;
|
|
2494
|
+
}
|
|
2495
|
+
else {
|
|
2496
|
+
this.domContentLoaded = true;
|
|
2497
|
+
this.loadFired = true;
|
|
2498
|
+
}
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
if (Date.now() > deadline) {
|
|
2502
|
+
throw new TimeoutError(`page.waitForLoadState: Timeout ${timeout}ms exceeded.`);
|
|
2503
|
+
}
|
|
2504
|
+
await delay(50);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
beginNavigationResponseCapture() {
|
|
2508
|
+
const capture = {
|
|
2509
|
+
lastResponse: null
|
|
2510
|
+
};
|
|
2511
|
+
this.navigationResponseCapture = capture;
|
|
2512
|
+
return capture;
|
|
2513
|
+
}
|
|
2514
|
+
async getResponseText(requestId) {
|
|
2515
|
+
if (!this.responseDataCollector) {
|
|
2516
|
+
return "";
|
|
2517
|
+
}
|
|
2518
|
+
const response = await this.client.networkGetData({
|
|
2519
|
+
collector: this.responseDataCollector,
|
|
2520
|
+
dataType: "response",
|
|
2521
|
+
request: requestId
|
|
2522
|
+
});
|
|
2523
|
+
return response.bytes.type === "base64"
|
|
2524
|
+
? Buffer.from(response.bytes.value, "base64").toString("utf8")
|
|
2525
|
+
: response.bytes.value;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
function shouldCaptureNavigationResponseUrl(url) {
|
|
2529
|
+
try {
|
|
2530
|
+
const protocol = new URL(url).protocol;
|
|
2531
|
+
return protocol === "http:" || protocol === "https:";
|
|
2532
|
+
}
|
|
2533
|
+
catch {
|
|
2534
|
+
return false;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
function formatSelectorChain(chain) {
|
|
2538
|
+
return chain
|
|
2539
|
+
.map((selector) => {
|
|
2540
|
+
if (selector.strategy === "css") {
|
|
2541
|
+
return selector.value;
|
|
2542
|
+
}
|
|
2543
|
+
if (selector.strategy === "xpath") {
|
|
2544
|
+
return `xpath=${selector.value}`;
|
|
2545
|
+
}
|
|
2546
|
+
if (selector.strategy === "text") {
|
|
2547
|
+
return `${selector.light ? "text:light" : "text"}=${selector.value}`;
|
|
2548
|
+
}
|
|
2549
|
+
return `${selector.strategy}=${selector.value}`;
|
|
2550
|
+
})
|
|
2551
|
+
.join(" >> ");
|
|
2552
|
+
}
|
|
2553
|
+
class BidiLocatorAdapter {
|
|
2554
|
+
page;
|
|
2555
|
+
state;
|
|
2556
|
+
constructor(page, state) {
|
|
2557
|
+
this.page = page;
|
|
2558
|
+
this.state = state;
|
|
2559
|
+
}
|
|
2560
|
+
locator(selector) {
|
|
2561
|
+
const { pick: _pick, ...stateWithoutPick } = this.state;
|
|
2562
|
+
const chain = this.state.pick
|
|
2563
|
+
? [...this.state.chain, locatorSelectorForPick(this.state.pick), selector]
|
|
2564
|
+
: [...this.state.chain, selector];
|
|
2565
|
+
return new BidiLocatorAdapter(this.page, {
|
|
2566
|
+
...stateWithoutPick,
|
|
2567
|
+
chain
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
getByText(text, options) {
|
|
2571
|
+
return this.locator(createTextLocatorSelector(text, options));
|
|
2572
|
+
}
|
|
2573
|
+
getByAltText(text, options) {
|
|
2574
|
+
return this.locator(createAltTextLocatorSelector(text, options));
|
|
2575
|
+
}
|
|
2576
|
+
getByLabel(text, options) {
|
|
2577
|
+
return this.locator(createLabelLocatorSelector(text, options));
|
|
2578
|
+
}
|
|
2579
|
+
getByPlaceholder(text, options) {
|
|
2580
|
+
return this.locator(createPlaceholderLocatorSelector(text, options));
|
|
2581
|
+
}
|
|
2582
|
+
getByTestId(testId) {
|
|
2583
|
+
return this.locator(createTestIdLocatorSelector(testId));
|
|
2584
|
+
}
|
|
2585
|
+
getByRole(role, options) {
|
|
2586
|
+
return this.locator(createRoleLocatorSelector(role, options));
|
|
2587
|
+
}
|
|
2588
|
+
getByTitle(text, options) {
|
|
2589
|
+
return this.locator(createTitleLocatorSelector(text, options));
|
|
2590
|
+
}
|
|
2591
|
+
first() {
|
|
2592
|
+
return new BidiLocatorAdapter(this.page, {
|
|
2593
|
+
...this.state,
|
|
2594
|
+
pick: { kind: "first" }
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
last() {
|
|
2598
|
+
return new BidiLocatorAdapter(this.page, {
|
|
2599
|
+
...this.state,
|
|
2600
|
+
pick: { kind: "last" }
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
nth(index) {
|
|
2604
|
+
return new BidiLocatorAdapter(this.page, {
|
|
2605
|
+
...this.state,
|
|
2606
|
+
pick: { kind: "nth", index }
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
async dblclick(options) {
|
|
2610
|
+
await this.page.dblclickLocator(this.state, options);
|
|
2611
|
+
}
|
|
2612
|
+
async check(options) {
|
|
2613
|
+
await this.page.checkLocator(this.state, options);
|
|
2614
|
+
}
|
|
2615
|
+
async click(options) {
|
|
2616
|
+
await this.page.clickLocator(this.state, options);
|
|
2617
|
+
}
|
|
2618
|
+
async hover(options) {
|
|
2619
|
+
await this.page.hoverLocator(this.state, options);
|
|
2620
|
+
}
|
|
2621
|
+
async fill(value, options) {
|
|
2622
|
+
await this.page.fillLocator(this.state, value, options);
|
|
2623
|
+
}
|
|
2624
|
+
async type(value, options) {
|
|
2625
|
+
await this.page.typeLocator(this.state, value, options);
|
|
2626
|
+
}
|
|
2627
|
+
async press(key, options) {
|
|
2628
|
+
await this.page.pressLocator(this.state, key, options);
|
|
2629
|
+
}
|
|
2630
|
+
async focus() {
|
|
2631
|
+
await this.page.focusLocator(this.state);
|
|
2632
|
+
}
|
|
2633
|
+
async blur() {
|
|
2634
|
+
await this.evaluate("(element) => element.blur()", undefined, true);
|
|
2635
|
+
}
|
|
2636
|
+
async count() {
|
|
2637
|
+
return this.page.countSelector({
|
|
2638
|
+
chain: this.state.chain,
|
|
2639
|
+
...(this.state.pick ? { pick: this.state.pick } : {})
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
async dispatchEvent(type, eventInit, options) {
|
|
2643
|
+
void options;
|
|
2644
|
+
await this.page.dispatchEvent(this.state.chain, type, eventInit);
|
|
2645
|
+
}
|
|
2646
|
+
async evaluate(expression, arg, isFunction) {
|
|
2647
|
+
return this.page.evaluateOnReference({
|
|
2648
|
+
chain: this.state.chain,
|
|
2649
|
+
...(this.state.pick ? { pick: this.state.pick } : {})
|
|
2650
|
+
}, expression, arg, `Could not resolve ${formatSelectorChain(this.state.chain)} to DOM Element`, isFunction);
|
|
2651
|
+
}
|
|
2652
|
+
async evaluateAll(expression, arg, isFunction) {
|
|
2653
|
+
return this.page.evaluateOnReferenceAll({
|
|
2654
|
+
chain: this.state.chain,
|
|
2655
|
+
...(this.state.pick ? { pick: this.state.pick } : {})
|
|
2656
|
+
}, expression, arg, isFunction);
|
|
2657
|
+
}
|
|
2658
|
+
async boundingBox() {
|
|
2659
|
+
return this.page.boundingBoxReference({
|
|
2660
|
+
chain: this.state.chain,
|
|
2661
|
+
...(this.state.pick ? { pick: this.state.pick } : {})
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
async getAttribute(name) {
|
|
2665
|
+
return this.page.getAttributeLocator(this.state, name);
|
|
2666
|
+
}
|
|
2667
|
+
async innerHTML() {
|
|
2668
|
+
return this.page.innerHTMLLocator(this.state);
|
|
2669
|
+
}
|
|
2670
|
+
async innerText() {
|
|
2671
|
+
return this.page.innerTextLocator(this.state);
|
|
2672
|
+
}
|
|
2673
|
+
async inputValue() {
|
|
2674
|
+
return this.page.inputValueLocator(this.state);
|
|
2675
|
+
}
|
|
2676
|
+
async isChecked() {
|
|
2677
|
+
return this.page.isCheckedLocator(this.state);
|
|
2678
|
+
}
|
|
2679
|
+
async isDisabled() {
|
|
2680
|
+
return this.page.isDisabledLocator(this.state);
|
|
2681
|
+
}
|
|
2682
|
+
async isEditable() {
|
|
2683
|
+
return this.page.isEditableLocator(this.state);
|
|
2684
|
+
}
|
|
2685
|
+
async isEnabled() {
|
|
2686
|
+
return this.page.isEnabledLocator(this.state);
|
|
2687
|
+
}
|
|
2688
|
+
async isHidden() {
|
|
2689
|
+
return !(await this.page.isVisibleLocator(this.state));
|
|
2690
|
+
}
|
|
2691
|
+
async textContent() {
|
|
2692
|
+
return this.page.textContentLocator(this.state);
|
|
2693
|
+
}
|
|
2694
|
+
async uncheck(options) {
|
|
2695
|
+
await this.page.uncheckLocator(this.state, options);
|
|
2696
|
+
}
|
|
2697
|
+
async selectOption(values, options) {
|
|
2698
|
+
return this.page.selectOptionLocator(this.state, values, options);
|
|
2699
|
+
}
|
|
2700
|
+
async isVisible() {
|
|
2701
|
+
return this.page.isVisibleLocator(this.state);
|
|
2702
|
+
}
|
|
2703
|
+
async screenshot(options) {
|
|
2704
|
+
void await this.elementHandle();
|
|
2705
|
+
return this.page.screenshot(options);
|
|
2706
|
+
}
|
|
2707
|
+
async scrollIntoViewIfNeeded() {
|
|
2708
|
+
await this.evaluate(SCROLL_INTO_VIEW_IF_NEEDED_SOURCE, undefined, true);
|
|
2709
|
+
}
|
|
2710
|
+
async selectText() {
|
|
2711
|
+
await this.evaluate(`(element) => {
|
|
2712
|
+
const retarget = (node) => {
|
|
2713
|
+
let target = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
2714
|
+
if (!target)
|
|
2715
|
+
throw new Error("Element is not attached to the DOM");
|
|
2716
|
+
if (!target.matches("input, textarea, select") && !target.isContentEditable)
|
|
2717
|
+
target = target.closest("button, [role=button], [role=checkbox], [role=radio]") || target;
|
|
2718
|
+
if (!target.matches("a, input, textarea, button, select, [role=link], [role=button], [role=checkbox], [role=radio]") && !target.isContentEditable) {
|
|
2719
|
+
const enclosingLabel = target.closest("label");
|
|
2720
|
+
if (enclosingLabel?.control)
|
|
2721
|
+
target = enclosingLabel.control;
|
|
2722
|
+
}
|
|
2723
|
+
return target;
|
|
2724
|
+
};
|
|
2725
|
+
const target = retarget(element);
|
|
2726
|
+
if (target instanceof HTMLInputElement) {
|
|
2727
|
+
target.select();
|
|
2728
|
+
target.focus();
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
if (target instanceof HTMLTextAreaElement) {
|
|
2732
|
+
target.selectionStart = 0;
|
|
2733
|
+
target.selectionEnd = target.value.length;
|
|
2734
|
+
target.focus();
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
if (typeof target.focus === "function")
|
|
2738
|
+
target.focus();
|
|
2739
|
+
const range = target.ownerDocument.createRange();
|
|
2740
|
+
range.selectNodeContents(target);
|
|
2741
|
+
const selection = target.ownerDocument.defaultView?.getSelection();
|
|
2742
|
+
if (selection) {
|
|
2743
|
+
selection.removeAllRanges();
|
|
2744
|
+
selection.addRange(range);
|
|
2745
|
+
}
|
|
2746
|
+
}`, undefined, true);
|
|
2747
|
+
}
|
|
2748
|
+
async tap(options) {
|
|
2749
|
+
await this.page.tap(this.state.chain, options);
|
|
2750
|
+
}
|
|
2751
|
+
async elementHandle() {
|
|
2752
|
+
const reference = {
|
|
2753
|
+
chain: this.state.chain,
|
|
2754
|
+
...(this.state.pick ? { pick: this.state.pick } : {})
|
|
2755
|
+
};
|
|
2756
|
+
const handleReference = await this.page.createHandleReference(reference, `Could not resolve ${formatSelectorChain(this.state.chain)} to DOM Element`);
|
|
2757
|
+
return this.page.createHandle(handleReference);
|
|
2758
|
+
}
|
|
2759
|
+
async elementHandles() {
|
|
2760
|
+
const count = await this.page.countSelector({
|
|
2761
|
+
chain: this.state.chain,
|
|
2762
|
+
...(this.state.pick ? { pick: this.state.pick } : {})
|
|
2763
|
+
});
|
|
2764
|
+
const handles = [];
|
|
2765
|
+
for (let index = 0; index < count; index += 1) {
|
|
2766
|
+
const reference = {
|
|
2767
|
+
chain: this.state.chain,
|
|
2768
|
+
pick: { kind: "nth", index }
|
|
2769
|
+
};
|
|
2770
|
+
handles.push(this.page.createHandle(await this.page.createHandleReference(reference)));
|
|
2771
|
+
}
|
|
2772
|
+
return handles;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
class BidiElementHandleAdapter {
|
|
2776
|
+
page;
|
|
2777
|
+
referenceState;
|
|
2778
|
+
constructor(page, referenceState) {
|
|
2779
|
+
this.page = page;
|
|
2780
|
+
this.referenceState = referenceState;
|
|
2781
|
+
}
|
|
2782
|
+
reference() {
|
|
2783
|
+
return {
|
|
2784
|
+
chain: [...this.referenceState.chain],
|
|
2785
|
+
...(this.referenceState.handleId ? { handleId: this.referenceState.handleId } : {}),
|
|
2786
|
+
...(this.referenceState.pick ? { pick: this.referenceState.pick } : {}),
|
|
2787
|
+
...(this.referenceState.scope ? { scope: this.referenceState.scope } : {})
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
async query(selector) {
|
|
2791
|
+
const reference = {
|
|
2792
|
+
scope: this.reference(),
|
|
2793
|
+
chain: selector
|
|
2794
|
+
};
|
|
2795
|
+
const count = await this.page.countSelector(reference);
|
|
2796
|
+
if (count === 0) {
|
|
2797
|
+
return null;
|
|
2798
|
+
}
|
|
2799
|
+
return new BidiElementHandleAdapter(this.page, {
|
|
2800
|
+
...reference,
|
|
2801
|
+
pick: { kind: "first" }
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
async queryAll(selector) {
|
|
2805
|
+
const reference = {
|
|
2806
|
+
scope: this.reference(),
|
|
2807
|
+
chain: selector
|
|
2808
|
+
};
|
|
2809
|
+
const count = await this.page.countSelector(reference);
|
|
2810
|
+
return Array.from({ length: count }, (_value, index) => {
|
|
2811
|
+
return new BidiElementHandleAdapter(this.page, {
|
|
2812
|
+
...reference,
|
|
2813
|
+
pick: { kind: "nth", index }
|
|
2814
|
+
});
|
|
2815
|
+
});
|
|
2816
|
+
}
|
|
2817
|
+
async evalOnSelector(selector, expression, isFunction, arg) {
|
|
2818
|
+
return this.page.evaluateOnReference({
|
|
2819
|
+
scope: this.reference(),
|
|
2820
|
+
chain: selector,
|
|
2821
|
+
pick: { kind: "first" }
|
|
2822
|
+
}, expression, arg, `elementHandle.$eval: Failed to find element matching selector "${formatSelectorChain(selector)}"`, isFunction);
|
|
2823
|
+
}
|
|
2824
|
+
async evalOnSelectorAll(selector, expression, isFunction, arg) {
|
|
2825
|
+
return this.page.evaluateOnReferenceAll({
|
|
2826
|
+
scope: this.reference(),
|
|
2827
|
+
chain: selector
|
|
2828
|
+
}, expression, arg, isFunction);
|
|
2829
|
+
}
|
|
2830
|
+
async evaluate(expression, arg) {
|
|
2831
|
+
return this.page.evaluateOnReference(this.reference(), expression, arg, "No element found.");
|
|
2832
|
+
}
|
|
2833
|
+
async boundingBox() {
|
|
2834
|
+
return this.page.boundingBoxReference(this.reference());
|
|
2835
|
+
}
|
|
2836
|
+
async dispatchEvent(type, eventInit) {
|
|
2837
|
+
await this.evaluate(`(element, payload) => {
|
|
2838
|
+
const createDOMEvent = (type, eventInit) => {
|
|
2839
|
+
const baseInit = {
|
|
2840
|
+
bubbles: true,
|
|
2841
|
+
cancelable: true,
|
|
2842
|
+
composed: true,
|
|
2843
|
+
...(eventInit && typeof eventInit === "object" ? eventInit : {})
|
|
2844
|
+
};
|
|
2845
|
+
if (type.startsWith("mouse") || type === "click" || type === "dblclick" || type === "contextmenu")
|
|
2846
|
+
return new MouseEvent(type, baseInit);
|
|
2847
|
+
if (type === "wheel")
|
|
2848
|
+
return new WheelEvent(type, baseInit);
|
|
2849
|
+
if (type.startsWith("drag") || type === "drop")
|
|
2850
|
+
return new DragEvent(type, baseInit);
|
|
2851
|
+
if (type.startsWith("key"))
|
|
2852
|
+
return new KeyboardEvent(type, baseInit);
|
|
2853
|
+
if (type === "input")
|
|
2854
|
+
return new InputEvent(type, baseInit);
|
|
2855
|
+
return new Event(type, baseInit);
|
|
2856
|
+
};
|
|
2857
|
+
const event = createDOMEvent(payload.type, payload.eventInit);
|
|
2858
|
+
element.dispatchEvent(event);
|
|
2859
|
+
}`, { type, eventInit });
|
|
2860
|
+
}
|
|
2861
|
+
async screenshot(options) {
|
|
2862
|
+
void await this.boundingBox();
|
|
2863
|
+
return this.page.screenshot(options);
|
|
2864
|
+
}
|
|
2865
|
+
async scrollIntoViewIfNeeded() {
|
|
2866
|
+
await this.evaluate(SCROLL_INTO_VIEW_IF_NEEDED_SOURCE, undefined);
|
|
2867
|
+
}
|
|
2868
|
+
async selectText() {
|
|
2869
|
+
await this.evaluate(`(element) => {
|
|
2870
|
+
const retarget = (node) => {
|
|
2871
|
+
let target = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
2872
|
+
if (!target)
|
|
2873
|
+
throw new Error("Element is not attached to the DOM");
|
|
2874
|
+
if (!target.matches("input, textarea, select") && !target.isContentEditable)
|
|
2875
|
+
target = target.closest("button, [role=button], [role=checkbox], [role=radio]") || target;
|
|
2876
|
+
if (!target.matches("a, input, textarea, button, select, [role=link], [role=button], [role=checkbox], [role=radio]") && !target.isContentEditable) {
|
|
2877
|
+
const enclosingLabel = target.closest("label");
|
|
2878
|
+
if (enclosingLabel?.control)
|
|
2879
|
+
target = enclosingLabel.control;
|
|
2880
|
+
}
|
|
2881
|
+
return target;
|
|
2882
|
+
};
|
|
2883
|
+
const target = retarget(element);
|
|
2884
|
+
if (target instanceof HTMLInputElement) {
|
|
2885
|
+
target.select();
|
|
2886
|
+
target.focus();
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
if (target instanceof HTMLTextAreaElement) {
|
|
2890
|
+
target.selectionStart = 0;
|
|
2891
|
+
target.selectionEnd = target.value.length;
|
|
2892
|
+
target.focus();
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
if (typeof target.focus === "function")
|
|
2896
|
+
target.focus();
|
|
2897
|
+
const range = target.ownerDocument.createRange();
|
|
2898
|
+
range.selectNodeContents(target);
|
|
2899
|
+
const selection = target.ownerDocument.defaultView?.getSelection();
|
|
2900
|
+
if (selection) {
|
|
2901
|
+
selection.removeAllRanges();
|
|
2902
|
+
selection.addRange(range);
|
|
2903
|
+
}
|
|
2904
|
+
}`, undefined);
|
|
2905
|
+
}
|
|
2906
|
+
async tap(options) {
|
|
2907
|
+
await this.page.tapReference(this.reference(), options);
|
|
2908
|
+
}
|
|
2909
|
+
async click(options) {
|
|
2910
|
+
await this.page.clickReference(this.reference(), options);
|
|
2911
|
+
}
|
|
2912
|
+
async dblclick(options) {
|
|
2913
|
+
await this.page.clickReference(this.reference(), { ...options, clickCount: 2 });
|
|
2914
|
+
}
|
|
2915
|
+
async check(options) {
|
|
2916
|
+
await this.page.setCheckedReference(this.reference(), true, options);
|
|
2917
|
+
}
|
|
2918
|
+
async hover(options) {
|
|
2919
|
+
await this.page.hoverReference(this.reference(), options);
|
|
2920
|
+
}
|
|
2921
|
+
async fill(value, options) {
|
|
2922
|
+
await this.page.fillReference(this.reference(), value, options);
|
|
2923
|
+
}
|
|
2924
|
+
async type(value, options) {
|
|
2925
|
+
await this.page.typeReference(this.reference(), value, options);
|
|
2926
|
+
}
|
|
2927
|
+
async press(key, options) {
|
|
2928
|
+
await this.page.pressReference(this.reference(), key, options);
|
|
2929
|
+
}
|
|
2930
|
+
async textContent() {
|
|
2931
|
+
return this.page.elementTextContent(this.reference());
|
|
2932
|
+
}
|
|
2933
|
+
async innerText() {
|
|
2934
|
+
return this.page.innerTextReference(this.reference());
|
|
2935
|
+
}
|
|
2936
|
+
async innerHTML() {
|
|
2937
|
+
return this.page.innerHTMLReference(this.reference());
|
|
2938
|
+
}
|
|
2939
|
+
async getAttribute(name) {
|
|
2940
|
+
return this.page.getAttributeReference(this.reference(), name);
|
|
2941
|
+
}
|
|
2942
|
+
async inputValue() {
|
|
2943
|
+
return this.page.inputValueReference(this.reference());
|
|
2944
|
+
}
|
|
2945
|
+
async isChecked() {
|
|
2946
|
+
return this.page.isCheckedReference(this.reference());
|
|
2947
|
+
}
|
|
2948
|
+
async isDisabled() {
|
|
2949
|
+
return this.page.isDisabledReference(this.reference());
|
|
2950
|
+
}
|
|
2951
|
+
async isEditable() {
|
|
2952
|
+
return this.page.isEditableReference(this.reference());
|
|
2953
|
+
}
|
|
2954
|
+
async isEnabled() {
|
|
2955
|
+
return this.page.isEnabledReference(this.reference());
|
|
2956
|
+
}
|
|
2957
|
+
async isHidden() {
|
|
2958
|
+
return !(await this.page.elementIsVisible(this.reference()));
|
|
2959
|
+
}
|
|
2960
|
+
async isVisible() {
|
|
2961
|
+
return this.page.elementIsVisible(this.reference());
|
|
2962
|
+
}
|
|
2963
|
+
async focus() {
|
|
2964
|
+
await this.page.focusReference(this.reference());
|
|
2965
|
+
}
|
|
2966
|
+
async uncheck(options) {
|
|
2967
|
+
await this.page.setCheckedReference(this.reference(), false, options);
|
|
2968
|
+
}
|
|
2969
|
+
async selectOption(values, options) {
|
|
2970
|
+
return this.page.selectOptionReference(this.reference(), values, options);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
function extractBiDiValue(value) {
|
|
2974
|
+
if (value.type === "array" && Array.isArray(value.value)) {
|
|
2975
|
+
return value.value.map((entry) => extractBiDiValue(entry));
|
|
2976
|
+
}
|
|
2977
|
+
// BiDi returns objects as arrays of [key, value] pairs
|
|
2978
|
+
if (value.type === "object" && Array.isArray(value.value)) {
|
|
2979
|
+
const obj = {};
|
|
2980
|
+
for (const [key, val] of value.value) {
|
|
2981
|
+
obj[key] = extractBiDiValue(val);
|
|
2982
|
+
}
|
|
2983
|
+
return obj;
|
|
2984
|
+
}
|
|
2985
|
+
return value.value;
|
|
2986
|
+
}
|
|
2987
|
+
function serializeForEvaluation(value) {
|
|
2988
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
2989
|
+
}
|
|
2990
|
+
function buttonNumber(button) {
|
|
2991
|
+
switch (button) {
|
|
2992
|
+
case "left":
|
|
2993
|
+
return 0;
|
|
2994
|
+
case "middle":
|
|
2995
|
+
return 1;
|
|
2996
|
+
case "right":
|
|
2997
|
+
return 2;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
function delay(ms) {
|
|
3001
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3002
|
+
}
|
|
3003
|
+
function hasContext(payload, contextId) {
|
|
3004
|
+
if (!payload || typeof payload !== "object" || !("context" in payload)) {
|
|
3005
|
+
return false;
|
|
3006
|
+
}
|
|
3007
|
+
return payload.context === contextId;
|
|
3008
|
+
}
|
|
3009
|
+
function hasLogContext(payload, contextId) {
|
|
3010
|
+
if (!payload || typeof payload !== "object" || !("source" in payload)) {
|
|
3011
|
+
return false;
|
|
3012
|
+
}
|
|
3013
|
+
return (payload.source?.context ===
|
|
3014
|
+
contextId);
|
|
3015
|
+
}
|
|
3016
|
+
function extractBiDiContextUrl(payload) {
|
|
3017
|
+
if (!payload || typeof payload !== "object" || !("url" in payload)) {
|
|
3018
|
+
return null;
|
|
3019
|
+
}
|
|
3020
|
+
const url = payload.url;
|
|
3021
|
+
return typeof url === "string" ? url : null;
|
|
3022
|
+
}
|
|
3023
|
+
function mapBiDiHeaders(headers) {
|
|
3024
|
+
return headers.map((header) => ({
|
|
3025
|
+
name: header.name,
|
|
3026
|
+
value: typeof header.value === "string"
|
|
3027
|
+
? header.value
|
|
3028
|
+
: header.value.value
|
|
3029
|
+
}));
|
|
3030
|
+
}
|
|
3031
|
+
function toBiDiOutgoingHeaders(headers) {
|
|
3032
|
+
return Object.entries(headers).map(([name, value]) => ({
|
|
3033
|
+
name,
|
|
3034
|
+
value: {
|
|
3035
|
+
type: "string",
|
|
3036
|
+
value
|
|
3037
|
+
}
|
|
3038
|
+
}));
|
|
3039
|
+
}
|
|
3040
|
+
function toBiDiKeyValue(key) {
|
|
3041
|
+
switch (key) {
|
|
3042
|
+
case "\r":
|
|
3043
|
+
case "\n":
|
|
3044
|
+
key = "Enter";
|
|
3045
|
+
break;
|
|
3046
|
+
}
|
|
3047
|
+
if ([...key].length === 1) {
|
|
3048
|
+
return key;
|
|
3049
|
+
}
|
|
3050
|
+
switch (key) {
|
|
3051
|
+
case "Cancel":
|
|
3052
|
+
return "\uE001";
|
|
3053
|
+
case "Help":
|
|
3054
|
+
return "\uE002";
|
|
3055
|
+
case "Backspace":
|
|
3056
|
+
return "\uE003";
|
|
3057
|
+
case "Tab":
|
|
3058
|
+
return "\uE004";
|
|
3059
|
+
case "Clear":
|
|
3060
|
+
return "\uE005";
|
|
3061
|
+
case "Enter":
|
|
3062
|
+
return "\uE007";
|
|
3063
|
+
case "Shift":
|
|
3064
|
+
case "ShiftLeft":
|
|
3065
|
+
return "\uE008";
|
|
3066
|
+
case "Control":
|
|
3067
|
+
case "ControlLeft":
|
|
3068
|
+
return "\uE009";
|
|
3069
|
+
case "Alt":
|
|
3070
|
+
case "AltLeft":
|
|
3071
|
+
return "\uE00A";
|
|
3072
|
+
case "Pause":
|
|
3073
|
+
return "\uE00B";
|
|
3074
|
+
case "Escape":
|
|
3075
|
+
return "\uE00C";
|
|
3076
|
+
case "PageUp":
|
|
3077
|
+
return "\uE00E";
|
|
3078
|
+
case "PageDown":
|
|
3079
|
+
return "\uE00F";
|
|
3080
|
+
case "End":
|
|
3081
|
+
return "\uE010";
|
|
3082
|
+
case "Home":
|
|
3083
|
+
return "\uE011";
|
|
3084
|
+
case "ArrowLeft":
|
|
3085
|
+
return "\uE012";
|
|
3086
|
+
case "ArrowUp":
|
|
3087
|
+
return "\uE013";
|
|
3088
|
+
case "ArrowRight":
|
|
3089
|
+
return "\uE014";
|
|
3090
|
+
case "ArrowDown":
|
|
3091
|
+
return "\uE015";
|
|
3092
|
+
case "Insert":
|
|
3093
|
+
return "\uE016";
|
|
3094
|
+
case "Delete":
|
|
3095
|
+
return "\uE017";
|
|
3096
|
+
case "NumpadEqual":
|
|
3097
|
+
return "\uE019";
|
|
3098
|
+
case "Numpad0":
|
|
3099
|
+
return "\uE01A";
|
|
3100
|
+
case "Numpad1":
|
|
3101
|
+
return "\uE01B";
|
|
3102
|
+
case "Numpad2":
|
|
3103
|
+
return "\uE01C";
|
|
3104
|
+
case "Numpad3":
|
|
3105
|
+
return "\uE01D";
|
|
3106
|
+
case "Numpad4":
|
|
3107
|
+
return "\uE01E";
|
|
3108
|
+
case "Numpad5":
|
|
3109
|
+
return "\uE01F";
|
|
3110
|
+
case "Numpad6":
|
|
3111
|
+
return "\uE020";
|
|
3112
|
+
case "Numpad7":
|
|
3113
|
+
return "\uE021";
|
|
3114
|
+
case "Numpad8":
|
|
3115
|
+
return "\uE022";
|
|
3116
|
+
case "Numpad9":
|
|
3117
|
+
return "\uE023";
|
|
3118
|
+
case "NumpadMultiply":
|
|
3119
|
+
return "\uE024";
|
|
3120
|
+
case "NumpadAdd":
|
|
3121
|
+
return "\uE025";
|
|
3122
|
+
case "NumpadSubtract":
|
|
3123
|
+
return "\uE027";
|
|
3124
|
+
case "NumpadDecimal":
|
|
3125
|
+
return "\uE028";
|
|
3126
|
+
case "NumpadDivide":
|
|
3127
|
+
return "\uE029";
|
|
3128
|
+
case "F1":
|
|
3129
|
+
return "\uE031";
|
|
3130
|
+
case "F2":
|
|
3131
|
+
return "\uE032";
|
|
3132
|
+
case "F3":
|
|
3133
|
+
return "\uE033";
|
|
3134
|
+
case "F4":
|
|
3135
|
+
return "\uE034";
|
|
3136
|
+
case "F5":
|
|
3137
|
+
return "\uE035";
|
|
3138
|
+
case "F6":
|
|
3139
|
+
return "\uE036";
|
|
3140
|
+
case "F7":
|
|
3141
|
+
return "\uE037";
|
|
3142
|
+
case "F8":
|
|
3143
|
+
return "\uE038";
|
|
3144
|
+
case "F9":
|
|
3145
|
+
return "\uE039";
|
|
3146
|
+
case "F10":
|
|
3147
|
+
return "\uE03A";
|
|
3148
|
+
case "F11":
|
|
3149
|
+
return "\uE03B";
|
|
3150
|
+
case "F12":
|
|
3151
|
+
return "\uE03C";
|
|
3152
|
+
case "Meta":
|
|
3153
|
+
case "MetaLeft":
|
|
3154
|
+
return "\uE03D";
|
|
3155
|
+
case "ShiftRight":
|
|
3156
|
+
return "\uE050";
|
|
3157
|
+
case "ControlRight":
|
|
3158
|
+
return "\uE051";
|
|
3159
|
+
case "AltRight":
|
|
3160
|
+
return "\uE052";
|
|
3161
|
+
case "MetaRight":
|
|
3162
|
+
return "\uE053";
|
|
3163
|
+
case "Space":
|
|
3164
|
+
return " ";
|
|
3165
|
+
case "Digit0":
|
|
3166
|
+
return "0";
|
|
3167
|
+
case "Digit1":
|
|
3168
|
+
return "1";
|
|
3169
|
+
case "Digit2":
|
|
3170
|
+
return "2";
|
|
3171
|
+
case "Digit3":
|
|
3172
|
+
return "3";
|
|
3173
|
+
case "Digit4":
|
|
3174
|
+
return "4";
|
|
3175
|
+
case "Digit5":
|
|
3176
|
+
return "5";
|
|
3177
|
+
case "Digit6":
|
|
3178
|
+
return "6";
|
|
3179
|
+
case "Digit7":
|
|
3180
|
+
return "7";
|
|
3181
|
+
case "Digit8":
|
|
3182
|
+
return "8";
|
|
3183
|
+
case "Digit9":
|
|
3184
|
+
return "9";
|
|
3185
|
+
case "KeyA":
|
|
3186
|
+
return "a";
|
|
3187
|
+
case "KeyB":
|
|
3188
|
+
return "b";
|
|
3189
|
+
case "KeyC":
|
|
3190
|
+
return "c";
|
|
3191
|
+
case "KeyD":
|
|
3192
|
+
return "d";
|
|
3193
|
+
case "KeyE":
|
|
3194
|
+
return "e";
|
|
3195
|
+
case "KeyF":
|
|
3196
|
+
return "f";
|
|
3197
|
+
case "KeyG":
|
|
3198
|
+
return "g";
|
|
3199
|
+
case "KeyH":
|
|
3200
|
+
return "h";
|
|
3201
|
+
case "KeyI":
|
|
3202
|
+
return "i";
|
|
3203
|
+
case "KeyJ":
|
|
3204
|
+
return "j";
|
|
3205
|
+
case "KeyK":
|
|
3206
|
+
return "k";
|
|
3207
|
+
case "KeyL":
|
|
3208
|
+
return "l";
|
|
3209
|
+
case "KeyM":
|
|
3210
|
+
return "m";
|
|
3211
|
+
case "KeyN":
|
|
3212
|
+
return "n";
|
|
3213
|
+
case "KeyO":
|
|
3214
|
+
return "o";
|
|
3215
|
+
case "KeyP":
|
|
3216
|
+
return "p";
|
|
3217
|
+
case "KeyQ":
|
|
3218
|
+
return "q";
|
|
3219
|
+
case "KeyR":
|
|
3220
|
+
return "r";
|
|
3221
|
+
case "KeyS":
|
|
3222
|
+
return "s";
|
|
3223
|
+
case "KeyT":
|
|
3224
|
+
return "t";
|
|
3225
|
+
case "KeyU":
|
|
3226
|
+
return "u";
|
|
3227
|
+
case "KeyV":
|
|
3228
|
+
return "v";
|
|
3229
|
+
case "KeyW":
|
|
3230
|
+
return "w";
|
|
3231
|
+
case "KeyX":
|
|
3232
|
+
return "x";
|
|
3233
|
+
case "KeyY":
|
|
3234
|
+
return "y";
|
|
3235
|
+
case "KeyZ":
|
|
3236
|
+
return "z";
|
|
3237
|
+
case "Semicolon":
|
|
3238
|
+
return ";";
|
|
3239
|
+
case "Equal":
|
|
3240
|
+
return "=";
|
|
3241
|
+
case "Comma":
|
|
3242
|
+
return ",";
|
|
3243
|
+
case "Minus":
|
|
3244
|
+
return "-";
|
|
3245
|
+
case "Period":
|
|
3246
|
+
return ".";
|
|
3247
|
+
case "Slash":
|
|
3248
|
+
return "/";
|
|
3249
|
+
case "Backquote":
|
|
3250
|
+
return "`";
|
|
3251
|
+
case "BracketLeft":
|
|
3252
|
+
return "[";
|
|
3253
|
+
case "Backslash":
|
|
3254
|
+
return "\\";
|
|
3255
|
+
case "BracketRight":
|
|
3256
|
+
return "]";
|
|
3257
|
+
case "Quote":
|
|
3258
|
+
return "\"";
|
|
3259
|
+
default:
|
|
3260
|
+
throw new Error(`Unknown key: "${key}"`);
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
function createScreencastHighlightBox(point) {
|
|
3264
|
+
const size = 48;
|
|
3265
|
+
return {
|
|
3266
|
+
left: point.x - size / 2,
|
|
3267
|
+
top: point.y - size / 2,
|
|
3268
|
+
width: size,
|
|
3269
|
+
height: size
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
async function waitForAbortableTimeout(duration, signal) {
|
|
3273
|
+
if (duration <= 0) {
|
|
3274
|
+
return !signal.aborted;
|
|
3275
|
+
}
|
|
3276
|
+
return await new Promise((resolve) => {
|
|
3277
|
+
const onAbort = () => {
|
|
3278
|
+
clearTimeout(timer);
|
|
3279
|
+
signal.removeEventListener("abort", onAbort);
|
|
3280
|
+
resolve(false);
|
|
3281
|
+
};
|
|
3282
|
+
const timer = setTimeout(() => {
|
|
3283
|
+
signal.removeEventListener("abort", onAbort);
|
|
3284
|
+
resolve(true);
|
|
3285
|
+
}, duration);
|
|
3286
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
function keyboardModifierState(modifiers) {
|
|
3290
|
+
return {
|
|
3291
|
+
altKey: modifiers.has("Alt"),
|
|
3292
|
+
ctrlKey: modifiers.has("Control"),
|
|
3293
|
+
metaKey: modifiers.has("Meta"),
|
|
3294
|
+
shiftKey: modifiers.has("Shift")
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
async function connectBidiFromWsEndpoint(wsEndpoint, sessionId) {
|
|
3298
|
+
const bidiEndpoint = buildFirefoxBidiEndpoint(wsEndpoint, sessionId);
|
|
3299
|
+
// Firefox BiDi endpoints are direct WebSocket connections and do not expose
|
|
3300
|
+
// CDP-style discovery endpoints such as /json/version.
|
|
3301
|
+
const client = await getBidiClientFactory()({
|
|
3302
|
+
webSocketUrl: bidiEndpoint,
|
|
3303
|
+
browserName: "firefox"
|
|
3304
|
+
});
|
|
3305
|
+
try {
|
|
3306
|
+
const ownsSession = await ensureBiDiSession(client, sessionId, wsEndpoint);
|
|
3307
|
+
return {
|
|
3308
|
+
client,
|
|
3309
|
+
ownsSession
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
catch (error) {
|
|
3313
|
+
client.close();
|
|
3314
|
+
throw error;
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
async function launchFirefoxBidi(options) {
|
|
3318
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "roxybrowser-bidi-"));
|
|
3319
|
+
const executableCandidates = resolveFirefoxExecutableCandidates(options, currentPlatform());
|
|
3320
|
+
let lastError;
|
|
3321
|
+
for (const executable of executableCandidates) {
|
|
3322
|
+
try {
|
|
3323
|
+
await assertFirefoxExecutable(executable);
|
|
3324
|
+
}
|
|
3325
|
+
catch (error) {
|
|
3326
|
+
if (options.executablePath) {
|
|
3327
|
+
throw error;
|
|
3328
|
+
}
|
|
3329
|
+
lastError = error;
|
|
3330
|
+
continue;
|
|
3331
|
+
}
|
|
3332
|
+
const host = options.host ?? "127.0.0.1";
|
|
3333
|
+
const port = options.port ?? await pickFreePort();
|
|
3334
|
+
const args = buildFirefoxLaunchArgs(options, userDataDir, port);
|
|
3335
|
+
const proc = spawn(executable, args, {
|
|
3336
|
+
detached: process.platform !== "win32",
|
|
3337
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3338
|
+
});
|
|
3339
|
+
const unregisterTestBrowserProcess = registerTestBrowserProcessForCleanup(proc, userDataDir);
|
|
3340
|
+
try {
|
|
3341
|
+
const wsEndpoint = await waitForFirefoxBiDiEndpoint(proc, host, port, 15_000);
|
|
3342
|
+
const connection = await connectBidiFromWsEndpoint(wsEndpoint, options.sessionId);
|
|
3343
|
+
return {
|
|
3344
|
+
client: connection.client,
|
|
3345
|
+
ownsSession: connection.ownsSession,
|
|
3346
|
+
process: proc,
|
|
3347
|
+
unregisterTestBrowserProcess,
|
|
3348
|
+
userDataDir
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3351
|
+
catch (error) {
|
|
3352
|
+
lastError = error;
|
|
3353
|
+
await cleanupFirefoxProcess(proc, userDataDir, unregisterTestBrowserProcess);
|
|
3354
|
+
if (options.executablePath) {
|
|
3355
|
+
throw error;
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
await rm(userDataDir, { force: true, recursive: true }).catch(() => { });
|
|
3360
|
+
throw lastError ?? new Error("Unable to launch a Firefox BiDi browser.");
|
|
3361
|
+
}
|
|
3362
|
+
async function cleanupFirefoxProcess(proc, userDataDir, unregisterTestBrowserProcess) {
|
|
3363
|
+
try {
|
|
3364
|
+
if (proc) {
|
|
3365
|
+
await terminateProcessTree(proc, { timeoutMs: CLEANUP_FIREFOX_PROCESS_TIMEOUT_MS });
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
finally {
|
|
3369
|
+
unregisterTestBrowserProcess?.();
|
|
3370
|
+
}
|
|
3371
|
+
if (userDataDir) {
|
|
3372
|
+
await rm(userDataDir, { force: true, recursive: true });
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
async function assertFirefoxExecutable(executable) {
|
|
3376
|
+
if (!isExplicitExecutablePath(executable)) {
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
try {
|
|
3380
|
+
await access(executable);
|
|
3381
|
+
}
|
|
3382
|
+
catch {
|
|
3383
|
+
throw new Error(`Firefox executable was not found at "${executable}". Pass executablePath or set ROXY_EXECUTABLE_PATH/ROXY_BIDI_EXECUTABLE_PATH to a Firefox binary with WebDriver BiDi support.`);
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
function completeUserURL(urlString) {
|
|
3387
|
+
if (/\s/.test(urlString)) {
|
|
3388
|
+
throw new Error(`Invalid URL: ${urlString}`);
|
|
3389
|
+
}
|
|
3390
|
+
if (urlString.startsWith("localhost") || urlString.startsWith("127.0.0.1")) {
|
|
3391
|
+
return `http://${urlString}`;
|
|
3392
|
+
}
|
|
3393
|
+
return urlString;
|
|
3394
|
+
}
|
|
3395
|
+
function shouldRetryFillActionabilityError(error) {
|
|
3396
|
+
const message = error instanceof Error
|
|
3397
|
+
? error.message.replace(/^(LocatorError:\s*)?(Error:\s*)?/, "").replace(/\s+Selector:.*$/s, "")
|
|
3398
|
+
: "";
|
|
3399
|
+
return (message === "No element found." ||
|
|
3400
|
+
message === "Element is not visible." ||
|
|
3401
|
+
message === "Element is not enabled." ||
|
|
3402
|
+
message === "Element is not editable.");
|
|
3403
|
+
}
|
|
3404
|
+
function verifyLifecycle(name, waitUntil) {
|
|
3405
|
+
if (waitUntil === "networkidle0") {
|
|
3406
|
+
waitUntil = "networkidle";
|
|
3407
|
+
}
|
|
3408
|
+
if (waitUntil !== "load" &&
|
|
3409
|
+
waitUntil !== "domcontentloaded" &&
|
|
3410
|
+
waitUntil !== "networkidle" &&
|
|
3411
|
+
waitUntil !== "commit") {
|
|
3412
|
+
throw new Error(`${name}: expected one of (load|domcontentloaded|networkidle|commit)`);
|
|
3413
|
+
}
|
|
3414
|
+
return waitUntil;
|
|
3415
|
+
}
|
|
3416
|
+
function isExplicitExecutablePath(executable) {
|
|
3417
|
+
return executable.includes("/") || executable.includes("\\");
|
|
3418
|
+
}
|
|
3419
|
+
function waitForFirefoxBiDiEndpoint(proc, host, port, timeoutMs) {
|
|
3420
|
+
return new Promise((resolve, reject) => {
|
|
3421
|
+
let settled = false;
|
|
3422
|
+
let stderr = "";
|
|
3423
|
+
let stdout = "";
|
|
3424
|
+
const finish = (callback) => {
|
|
3425
|
+
if (settled)
|
|
3426
|
+
return;
|
|
3427
|
+
settled = true;
|
|
3428
|
+
clearTimeout(timer);
|
|
3429
|
+
proc.stderr?.off("data", onStderr);
|
|
3430
|
+
proc.stdout?.off("data", onStdout);
|
|
3431
|
+
proc.off("error", onError);
|
|
3432
|
+
proc.off("exit", onExit);
|
|
3433
|
+
callback();
|
|
3434
|
+
};
|
|
3435
|
+
const maybeResolveFromOutput = () => {
|
|
3436
|
+
const output = `${stderr}\n${stdout}`;
|
|
3437
|
+
const endpoint = output.match(/WebDriver BiDi listening on (ws:\/\/[^\s]+)/)?.[1];
|
|
3438
|
+
if (endpoint) {
|
|
3439
|
+
finish(() => resolve(endpoint));
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
if (output.includes("WebDriver BiDi listening")) {
|
|
3443
|
+
finish(() => resolve(`ws://${host}:${port}`));
|
|
3444
|
+
}
|
|
3445
|
+
};
|
|
3446
|
+
const onStderr = (chunk) => {
|
|
3447
|
+
stderr += String(chunk);
|
|
3448
|
+
maybeResolveFromOutput();
|
|
3449
|
+
};
|
|
3450
|
+
const onStdout = (chunk) => {
|
|
3451
|
+
stdout += String(chunk);
|
|
3452
|
+
maybeResolveFromOutput();
|
|
3453
|
+
};
|
|
3454
|
+
const onError = (error) => {
|
|
3455
|
+
finish(() => reject(error instanceof Error ? error : new Error(String(error))));
|
|
3456
|
+
};
|
|
3457
|
+
const onExit = () => {
|
|
3458
|
+
finish(() => reject(new Error(stderr || stdout
|
|
3459
|
+
? `Firefox exited before exposing BiDi endpoint:\nstderr: ${stderr.trim()}\nstdout: ${stdout.trim()}`
|
|
3460
|
+
: "Firefox exited before exposing BiDi endpoint.")));
|
|
3461
|
+
};
|
|
3462
|
+
const timer = setTimeout(() => {
|
|
3463
|
+
finish(() => reject(new TimeoutError(`Timed out waiting for Firefox BiDi endpoint.\nstderr: ${stderr.trim()}\nstdout: ${stdout.trim()}`)));
|
|
3464
|
+
}, timeoutMs);
|
|
3465
|
+
proc.stderr?.on("data", onStderr);
|
|
3466
|
+
proc.stdout?.on("data", onStdout);
|
|
3467
|
+
proc.once("error", onError);
|
|
3468
|
+
proc.once("exit", onExit);
|
|
3469
|
+
});
|
|
3470
|
+
}
|
|
3471
|
+
async function pickFreePort() {
|
|
3472
|
+
const { createServer } = await import("node:net");
|
|
3473
|
+
return new Promise((resolve, reject) => {
|
|
3474
|
+
const server = createServer();
|
|
3475
|
+
server.listen(0, "127.0.0.1", () => {
|
|
3476
|
+
const address = server.address();
|
|
3477
|
+
server.close(() => {
|
|
3478
|
+
if (address && typeof address === "object") {
|
|
3479
|
+
resolve(address.port);
|
|
3480
|
+
}
|
|
3481
|
+
else {
|
|
3482
|
+
reject(new Error("Failed to pick a free port."));
|
|
3483
|
+
}
|
|
3484
|
+
});
|
|
3485
|
+
});
|
|
3486
|
+
});
|
|
3487
|
+
}
|
|
3488
|
+
function buildFirefoxBidiEndpoint(wsEndpoint, sessionId) {
|
|
3489
|
+
const url = new URL(wsEndpoint);
|
|
3490
|
+
if (sessionId) {
|
|
3491
|
+
url.pathname = `/session/${sessionId}`;
|
|
3492
|
+
return url.toString();
|
|
3493
|
+
}
|
|
3494
|
+
if (url.pathname === "/" || url.pathname === "") {
|
|
3495
|
+
url.pathname = "/session";
|
|
3496
|
+
}
|
|
3497
|
+
return url.toString();
|
|
3498
|
+
}
|
|
3499
|
+
function isSessionSpecificFirefoxBidiEndpoint(wsEndpoint) {
|
|
3500
|
+
const pathname = new URL(wsEndpoint).pathname;
|
|
3501
|
+
return /^\/session\/[^/]+$/.test(pathname);
|
|
3502
|
+
}
|
|
3503
|
+
async function ensureBiDiSession(client, sessionId, wsEndpoint) {
|
|
3504
|
+
await client.sessionStatus({});
|
|
3505
|
+
if (sessionId || isSessionSpecificFirefoxBidiEndpoint(wsEndpoint)) {
|
|
3506
|
+
return false;
|
|
3507
|
+
}
|
|
3508
|
+
try {
|
|
3509
|
+
await client.browsingContextGetTree({});
|
|
3510
|
+
return false;
|
|
3511
|
+
}
|
|
3512
|
+
catch (error) {
|
|
3513
|
+
if (!String(error instanceof Error ? error.message : error).includes("session does not exist")) {
|
|
3514
|
+
throw error;
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
try {
|
|
3518
|
+
await client.sessionNew({
|
|
3519
|
+
capabilities: {
|
|
3520
|
+
alwaysMatch: {
|
|
3521
|
+
acceptInsecureCerts: true
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
});
|
|
3525
|
+
return true;
|
|
3526
|
+
}
|
|
3527
|
+
catch (error) {
|
|
3528
|
+
const message = String(error instanceof Error ? error.message : error);
|
|
3529
|
+
if (message.includes("Maximum number of active sessions")) {
|
|
3530
|
+
throw new Error("Maximum number of active BiDi sessions. Reuse an existing one with sessionId or close the current session first.");
|
|
3531
|
+
}
|
|
3532
|
+
throw error;
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
export function buildFirefoxLaunchArgs(options, userDataDir, port) {
|
|
3536
|
+
return [
|
|
3537
|
+
"-profile",
|
|
3538
|
+
userDataDir,
|
|
3539
|
+
"-no-remote",
|
|
3540
|
+
`--remote-debugging-port=${port}`,
|
|
3541
|
+
...(options.headless === false ? [] : ["-headless"]),
|
|
3542
|
+
...(options.args ?? [])
|
|
3543
|
+
];
|
|
3544
|
+
}
|
|
3545
|
+
export function resolveFirefoxExecutableCandidates(options, platform = currentPlatform(), fileExistsFn = fileExists) {
|
|
3546
|
+
if (options.executablePath) {
|
|
3547
|
+
return [options.executablePath];
|
|
3548
|
+
}
|
|
3549
|
+
return filterExistingFirefoxExecutableCandidates(defaultFirefoxExecutableCandidates(platform), platform, fileExistsFn);
|
|
3550
|
+
}
|
|
3551
|
+
function defaultFirefoxExecutableCandidates(platform) {
|
|
3552
|
+
const candidates = [];
|
|
3553
|
+
switch (platform) {
|
|
3554
|
+
case "darwin":
|
|
3555
|
+
candidates.push("/Applications/Firefox.app/Contents/MacOS/firefox", "/Applications/Firefox Nightly.app/Contents/MacOS/firefox");
|
|
3556
|
+
return candidates;
|
|
3557
|
+
case "win32":
|
|
3558
|
+
candidates.push("C:\\Program Files\\Mozilla Firefox\\firefox.exe", "C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe");
|
|
3559
|
+
return candidates;
|
|
3560
|
+
default:
|
|
3561
|
+
candidates.push("firefox");
|
|
3562
|
+
return candidates;
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
function filterExistingFirefoxExecutableCandidates(candidates, platform, fileExistsFn) {
|
|
3566
|
+
if (platform === "linux") {
|
|
3567
|
+
return candidates;
|
|
3568
|
+
}
|
|
3569
|
+
const existing = candidates.filter(fileExistsFn);
|
|
3570
|
+
return existing.length > 0 ? existing : candidates;
|
|
3571
|
+
}
|
|
3572
|
+
function fileExists(path) {
|
|
3573
|
+
try {
|
|
3574
|
+
accessSync(path, fsConstants.F_OK);
|
|
3575
|
+
return true;
|
|
3576
|
+
}
|
|
3577
|
+
catch {
|
|
3578
|
+
return false;
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
function currentPlatform() {
|
|
3582
|
+
return process.platform;
|
|
3583
|
+
}
|
|
3584
|
+
function isSelectOptionRetryResult(value) {
|
|
3585
|
+
return !Array.isArray(value) && value.__needsRetry === true;
|
|
3586
|
+
}
|
|
3587
|
+
function normalizeConsoleMessageType(value) {
|
|
3588
|
+
if (value === "warn") {
|
|
3589
|
+
return "warning";
|
|
3590
|
+
}
|
|
3591
|
+
switch (value) {
|
|
3592
|
+
case "log":
|
|
3593
|
+
case "debug":
|
|
3594
|
+
case "info":
|
|
3595
|
+
case "error":
|
|
3596
|
+
case "warning":
|
|
3597
|
+
case "dir":
|
|
3598
|
+
case "dirxml":
|
|
3599
|
+
case "table":
|
|
3600
|
+
case "trace":
|
|
3601
|
+
case "clear":
|
|
3602
|
+
case "startGroup":
|
|
3603
|
+
case "startGroupCollapsed":
|
|
3604
|
+
case "endGroup":
|
|
3605
|
+
case "assert":
|
|
3606
|
+
case "profile":
|
|
3607
|
+
case "profileEnd":
|
|
3608
|
+
case "count":
|
|
3609
|
+
case "time":
|
|
3610
|
+
case "timeEnd":
|
|
3611
|
+
return value;
|
|
3612
|
+
default:
|
|
3613
|
+
return "log";
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
//# sourceMappingURL=backend.js.map
|