Merge branch 'microsoft:main' into add-copy-button-annotations

This commit is contained in:
Anthony Roberts 2024-09-13 13:36:27 +10:00 committed by GitHub
commit c4a48d2983
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
348 changed files with 43612 additions and 5603 deletions

View file

@ -24,6 +24,7 @@ body:
## Make a minimal reproduction ## Make a minimal reproduction
To file the report, you will need a GitHub repository with a minimal (but complete) example and simple/clear steps on how to reproduce the bug. To file the report, you will need a GitHub repository with a minimal (but complete) example and simple/clear steps on how to reproduce the bug.
The simpler you can make it, the more likely we are to successfully verify and fix the bug. You can create a new project with `npm init playwright@latest new-project` and then add the test code there. The simpler you can make it, the more likely we are to successfully verify and fix the bug. You can create a new project with `npm init playwright@latest new-project` and then add the test code there.
Please make sure you only include the code and the dependencies absolutely necessary for your repro. Due to the security considerations, we can only run the code we trust. Major web frameworks are Ok to use, but smaller convenience libraries are not.
- type: markdown - type: markdown
attributes: attributes:
value: | value: |

46
.github/workflows/tests_bidi.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: tests BiDi
on:
workflow_dispatch:
pull_request:
branches:
- main
paths:
- .github/workflows/tests_bidi.yml
schedule:
# Run every day at midnight
- cron: '0 0 * * *'
env:
FORCE_COLOR: 1
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
jobs:
test_bidi:
name: BiDi
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ubuntu-24.04
permissions:
id-token: write # This is required for OIDC login (azure/login) to succeed
contents: read # This is required for actions/checkout to succeed
strategy:
fail-fast: false
matrix:
channel: [bidi-chromium, bidi-firefox-beta]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
- run: npm run build
- run: npx playwright install --with-deps chromium
if: matrix.channel == 'bidi-chromium'
- run: npx -y @puppeteer/browsers install firefox@beta
if: matrix.channel == 'bidi-firefox-beta'
- name: Run tests
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run biditest -- --project=${{ matrix.channel }}*
env:
PWTEST_USE_BIDI_EXPECTATIONS: '1'

View file

@ -50,7 +50,9 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [macos-12, macos-13, macos-14] # Intel: macos-13, macos-14-large
# Arm64: macos-13-xlarge, macos-14
os: [macos-13, macos-13-xlarge, macos-14-large, macos-14]
browser: [chromium, firefox, webkit] browser: [chromium, firefox, webkit]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
@ -235,7 +237,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-20.04, macos-12, windows-latest] os: [ubuntu-20.04, macos-13, windows-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ./.github/actions/run-test - uses: ./.github/actions/run-test

View file

@ -53,7 +53,7 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build - run: npm run build
- name: Download blob report artifact - name: Download blob report artifact
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
with: with:
name: all-blob-reports name: all-blob-reports
path: all-blob-reports path: all-blob-reports

1
.gitignore vendored
View file

@ -34,3 +34,4 @@ test-results
/tests/installation/.registry.json /tests/installation/.registry.json
.cache/ .cache/
.eslintcache .eslintcache
playwright.env

View file

@ -1,6 +1,6 @@
# 🎭 Playwright # 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-128.0.6613.36-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-129.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) [![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-129.0.6668.42-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-130.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@ -8,9 +8,9 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows | | | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: | | :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->128.0.6613.36<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Chromium <!-- GEN:chromium-version -->129.0.6668.42<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->129.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox <!-- GEN:firefox-version -->130.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details. Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.

17
SUPPORT.md Normal file
View file

@ -0,0 +1,17 @@
# Support
## How to file issues and get help
This project uses GitHub issues to track bugs and feature requests. Please search the [existing issues][gh-issues] before filing new ones to avoid duplicates. For new issues, file your bug or feature request as a new issue using corresponding template.
For help and questions about using this project, please see the [docs site for Playwright][docs].
Join our community [Discord Server][discord-server] to connect with other developers using Playwright and ask questions in our 'help-playwright' forum.
## Microsoft Support Policy
Support for Playwright is limited to the resources listed above.
[gh-issues]: https://github.com/microsoft/playwright/issues/
[docs]: https://playwright.dev/
[discord-server]: https://aka.ms/playwright/discord

View file

@ -1,3 +1,3 @@
REMOTE_URL="https://github.com/mozilla/gecko-dev" REMOTE_URL="https://github.com/mozilla/gecko-dev"
BASE_BRANCH="release" BASE_BRANCH="release"
BASE_REVISION="4c9a3f8e2db68ae0a8fcf6bbf0574e3c0549ff49" BASE_REVISION="cf0397e3ba298868fdca53f894da5b0d239dc09e"

View file

@ -602,6 +602,8 @@ class NetworkObserver {
proxyFilter.onProxyFilterResult(defaultProxyInfo); proxyFilter.onProxyFilterResult(defaultProxyInfo);
return; return;
} }
if (this._targetRegistry.shouldBustHTTPAuthCacheForProxy(proxy))
Services.obs.notifyObservers(null, "net:clear-active-logins");
proxyFilter.onProxyFilterResult(protocolProxyService.newProxyInfo( proxyFilter.onProxyFilterResult(protocolProxyService.newProxyInfo(
proxy.type, proxy.type,
proxy.host, proxy.host,

View file

@ -116,6 +116,7 @@ class TargetRegistry {
this._browserToTarget = new Map(); this._browserToTarget = new Map();
this._browserIdToTarget = new Map(); this._browserIdToTarget = new Map();
this._proxiesWithClashingAuthCacheKeys = new Set();
this._browserProxy = null; this._browserProxy = null;
// Cleanup containers from previous runs (if any) // Cleanup containers from previous runs (if any)
@ -234,12 +235,50 @@ class TargetRegistry {
onOpenWindow(win); onOpenWindow(win);
} }
// Firefox uses nsHttpAuthCache to cache authentication to the proxy.
// If we're provided with a single proxy with a multiple different authentications, then
// we should clear the nsHttpAuthCache on every request.
shouldBustHTTPAuthCacheForProxy(proxy) {
return this._proxiesWithClashingAuthCacheKeys.has(proxy);
}
_updateProxiesWithSameAuthCacheAndDifferentCredentials() {
const proxyIdToCredentials = new Map();
const allProxies = [...this._browserContextIdToBrowserContext.values()].map(bc => bc._proxy).filter(Boolean);
if (this._browserProxy)
allProxies.push(this._browserProxy);
const proxyAuthCacheKeyAndProxy = allProxies.map(proxy => [
JSON.stringify({
type: proxy.type,
host: proxy.host,
port: proxy.port,
}),
proxy,
]);
this._proxiesWithClashingAuthCacheKeys.clear();
proxyAuthCacheKeyAndProxy.sort(([cacheKey1], [cacheKey2]) => cacheKey1 < cacheKey2 ? -1 : 1);
for (let i = 0; i < proxyAuthCacheKeyAndProxy.length - 1; ++i) {
const [cacheKey1, proxy1] = proxyAuthCacheKeyAndProxy[i];
const [cacheKey2, proxy2] = proxyAuthCacheKeyAndProxy[i + 1];
if (cacheKey1 !== cacheKey2)
continue;
if (proxy1.username === proxy2.username && proxy1.password === proxy2.password)
continue;
// `proxy1` and `proxy2` have the same caching key, but serve different credentials.
// We have to bust HTTP Auth Cache everytime there's a request that will use either of the proxies.
this._proxiesWithClashingAuthCacheKeys.add(proxy1);
this._proxiesWithClashingAuthCacheKeys.add(proxy2);
}
}
async cancelDownload(options) { async cancelDownload(options) {
this._downloadInterceptor.cancelDownload(options.uuid); this._downloadInterceptor.cancelDownload(options.uuid);
} }
setBrowserProxy(proxy) { setBrowserProxy(proxy) {
this._browserProxy = proxy; this._browserProxy = proxy;
this._updateProxiesWithSameAuthCacheAndDifferentCredentials();
} }
getProxyInfo(channel) { getProxyInfo(channel) {
@ -906,12 +945,14 @@ class BrowserContext {
} }
this._registry._browserContextIdToBrowserContext.delete(this.browserContextId); this._registry._browserContextIdToBrowserContext.delete(this.browserContextId);
this._registry._userContextIdToBrowserContext.delete(this.userContextId); this._registry._userContextIdToBrowserContext.delete(this.userContextId);
this._registry._updateProxiesWithSameAuthCacheAndDifferentCredentials();
} }
setProxy(proxy) { setProxy(proxy) {
// Clear AuthCache. // Clear AuthCache.
Services.obs.notifyObservers(null, "net:clear-active-logins"); Services.obs.notifyObservers(null, "net:clear-active-logins");
this._proxy = proxy; this._proxy = proxy;
this._registry._updateProxiesWithSameAuthCacheAndDifferentCredentials();
} }
setIgnoreHTTPSErrors(ignoreHTTPSErrors) { setIgnoreHTTPSErrors(ignoreHTTPSErrors) {

View file

@ -70,7 +70,7 @@ class JugglerFrameChild extends JSWindowActorChild {
const agents = topBrowingContextToAgents.get(this.browsingContext); const agents = topBrowingContextToAgents.get(this.browsingContext);
// The agents are already re-bound to a new actor. // The agents are already re-bound to a new actor.
if (agents.actor !== this) if (agents?.actor !== this)
return; return;
topBrowingContextToAgents.delete(this.browsingContext); topBrowingContextToAgents.delete(this.browsingContext);

View file

@ -129,7 +129,7 @@ class nsScreencastService::Session : public rtc::VideoSinkInterface<webrtc::Vide
capability.height = 960; capability.height = 960;
capability.maxFPS = ScreencastEncoder::fps; capability.maxFPS = ScreencastEncoder::fps;
capability.videoType = webrtc::VideoType::kI420; capability.videoType = webrtc::VideoType::kI420;
int error = mCaptureModule->StartCapture(capability); int error = mCaptureModule->StartCaptureCounted(capability);
if (error) { if (error) {
fprintf(stderr, "StartCapture error %d\n", error); fprintf(stderr, "StartCapture error %d\n", error);
return false; return false;
@ -152,7 +152,7 @@ class nsScreencastService::Session : public rtc::VideoSinkInterface<webrtc::Vide
mCaptureModule->DeRegisterCaptureDataCallback(this); mCaptureModule->DeRegisterCaptureDataCallback(this);
else else
mCaptureModule->DeRegisterRawFrameCallback(this); mCaptureModule->DeRegisterRawFrameCallback(this);
mCaptureModule->StopCapture(); mCaptureModule->StopCaptureCounted();
if (mEncoder) { if (mEncoder) {
mEncoder->finish([this, protect = RefPtr{this}] { mEncoder->finish([this, protect = RefPtr{this}] {
NS_DispatchToMainThread(NS_NewRunnableFunction( NS_DispatchToMainThread(NS_NewRunnableFunction(

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,3 @@
REMOTE_URL="https://github.com/WebKit/WebKit.git" REMOTE_URL="https://github.com/WebKit/WebKit.git"
BASE_BRANCH="main" BASE_BRANCH="main"
BASE_REVISION="a47deb713746fa2f228e8450a52ed0ecafc5309d" BASE_REVISION="f371dbc2bb4292037ed394e2162150a16ef977fc"

File diff suppressed because it is too large Load diff

View file

@ -141,7 +141,7 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.delete.params = %%-js-fetch-option-params-%% ### option: APIRequestContext.delete.params = %%-js-fetch-option-params-%%
* since: v1.16 * since: v1.16
### param: APIRequestContext.delete.params = %%-java-csharp-fetch-params-%% ### param: APIRequestContext.delete.params = %%-java-fetch-params-%%
* since: v1.18 * since: v1.18
### option: APIRequestContext.delete.params = %%-python-fetch-option-params-%% ### option: APIRequestContext.delete.params = %%-python-fetch-option-params-%%
@ -150,6 +150,9 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.delete.params = %%-csharp-fetch-option-params-%% ### option: APIRequestContext.delete.params = %%-csharp-fetch-option-params-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.delete.paramsString = %%-csharp-fetch-option-paramsString-%%
* since: v1.47
### option: APIRequestContext.delete.headers = %%-js-python-csharp-fetch-option-headers-%% ### option: APIRequestContext.delete.headers = %%-js-python-csharp-fetch-option-headers-%%
* since: v1.16 * since: v1.16
@ -303,7 +306,7 @@ Target URL or Request to get all parameters from.
### option: APIRequestContext.fetch.params = %%-js-fetch-option-params-%% ### option: APIRequestContext.fetch.params = %%-js-fetch-option-params-%%
* since: v1.16 * since: v1.16
### param: APIRequestContext.fetch.params = %%-java-csharp-fetch-params-%% ### param: APIRequestContext.fetch.params = %%-java-fetch-params-%%
* since: v1.18 * since: v1.18
### option: APIRequestContext.fetch.params = %%-python-fetch-option-params-%% ### option: APIRequestContext.fetch.params = %%-python-fetch-option-params-%%
@ -312,6 +315,9 @@ Target URL or Request to get all parameters from.
### option: APIRequestContext.fetch.params = %%-csharp-fetch-option-params-%% ### option: APIRequestContext.fetch.params = %%-csharp-fetch-option-params-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.fetch.paramsString = %%-csharp-fetch-option-paramsString-%%
* since: v1.47
### option: APIRequestContext.fetch.method ### option: APIRequestContext.fetch.method
* since: v1.16 * since: v1.16
* langs: js, python, csharp * langs: js, python, csharp
@ -418,7 +424,7 @@ await request.GetAsync("https://example.com/api/getText", new() { Params = query
### option: APIRequestContext.get.params = %%-js-fetch-option-params-%% ### option: APIRequestContext.get.params = %%-js-fetch-option-params-%%
* since: v1.16 * since: v1.16
### param: APIRequestContext.get.params = %%-java-csharp-fetch-params-%% ### param: APIRequestContext.get.params = %%-java-fetch-params-%%
* since: v1.18 * since: v1.18
### option: APIRequestContext.get.params = %%-python-fetch-option-params-%% ### option: APIRequestContext.get.params = %%-python-fetch-option-params-%%
@ -427,6 +433,9 @@ await request.GetAsync("https://example.com/api/getText", new() { Params = query
### option: APIRequestContext.get.params = %%-csharp-fetch-option-params-%% ### option: APIRequestContext.get.params = %%-csharp-fetch-option-params-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.get.paramsString = %%-csharp-fetch-option-paramsString-%%
* since: v1.47
### option: APIRequestContext.get.headers = %%-js-python-csharp-fetch-option-headers-%% ### option: APIRequestContext.get.headers = %%-js-python-csharp-fetch-option-headers-%%
* since: v1.16 * since: v1.16
@ -477,7 +486,7 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.head.params = %%-js-fetch-option-params-%% ### option: APIRequestContext.head.params = %%-js-fetch-option-params-%%
* since: v1.16 * since: v1.16
### param: APIRequestContext.head.params = %%-java-csharp-fetch-params-%% ### param: APIRequestContext.head.params = %%-java-fetch-params-%%
* since: v1.18 * since: v1.18
### option: APIRequestContext.head.params = %%-python-fetch-option-params-%% ### option: APIRequestContext.head.params = %%-python-fetch-option-params-%%
@ -486,6 +495,9 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.head.params = %%-csharp-fetch-option-params-%% ### option: APIRequestContext.head.params = %%-csharp-fetch-option-params-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.head.paramsString = %%-csharp-fetch-option-paramsString-%%
* since: v1.47
### option: APIRequestContext.head.headers = %%-js-python-csharp-fetch-option-headers-%% ### option: APIRequestContext.head.headers = %%-js-python-csharp-fetch-option-headers-%%
* since: v1.16 * since: v1.16
@ -536,7 +548,7 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.patch.params = %%-js-fetch-option-params-%% ### option: APIRequestContext.patch.params = %%-js-fetch-option-params-%%
* since: v1.16 * since: v1.16
### param: APIRequestContext.patch.params = %%-java-csharp-fetch-params-%% ### param: APIRequestContext.patch.params = %%-java-fetch-params-%%
* since: v1.18 * since: v1.18
### option: APIRequestContext.patch.params = %%-python-fetch-option-params-%% ### option: APIRequestContext.patch.params = %%-python-fetch-option-params-%%
@ -545,6 +557,9 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.patch.params = %%-csharp-fetch-option-params-%% ### option: APIRequestContext.patch.params = %%-csharp-fetch-option-params-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.patch.paramsString = %%-csharp-fetch-option-paramsString-%%
* since: v1.47
### option: APIRequestContext.patch.headers = %%-js-python-csharp-fetch-option-headers-%% ### option: APIRequestContext.patch.headers = %%-js-python-csharp-fetch-option-headers-%%
* since: v1.16 * since: v1.16
@ -716,7 +731,7 @@ await request.PostAsync("https://example.com/api/uploadScript", new() { Multipar
### option: APIRequestContext.post.params = %%-js-fetch-option-params-%% ### option: APIRequestContext.post.params = %%-js-fetch-option-params-%%
* since: v1.16 * since: v1.16
### param: APIRequestContext.post.params = %%-java-csharp-fetch-params-%% ### param: APIRequestContext.post.params = %%-java-fetch-params-%%
* since: v1.18 * since: v1.18
### option: APIRequestContext.post.params = %%-python-fetch-option-params-%% ### option: APIRequestContext.post.params = %%-python-fetch-option-params-%%
@ -725,6 +740,9 @@ await request.PostAsync("https://example.com/api/uploadScript", new() { Multipar
### option: APIRequestContext.post.params = %%-csharp-fetch-option-params-%% ### option: APIRequestContext.post.params = %%-csharp-fetch-option-params-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.post.paramsString = %%-csharp-fetch-option-paramsString-%%
* since: v1.47
### option: APIRequestContext.post.headers = %%-js-python-csharp-fetch-option-headers-%% ### option: APIRequestContext.post.headers = %%-js-python-csharp-fetch-option-headers-%%
* since: v1.16 * since: v1.16
@ -775,7 +793,7 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.put.params = %%-js-fetch-option-params-%% ### option: APIRequestContext.put.params = %%-js-fetch-option-params-%%
* since: v1.16 * since: v1.16
### param: APIRequestContext.put.params = %%-java-csharp-fetch-params-%% ### param: APIRequestContext.put.params = %%-java-fetch-params-%%
* since: v1.18 * since: v1.18
### option: APIRequestContext.put.params = %%-python-fetch-option-params-%% ### option: APIRequestContext.put.params = %%-python-fetch-option-params-%%
@ -784,6 +802,9 @@ context cookies from the response. The method will automatically follow redirect
### option: APIRequestContext.put.params = %%-csharp-fetch-option-params-%% ### option: APIRequestContext.put.params = %%-csharp-fetch-option-params-%%
* since: v1.16 * since: v1.16
### option: APIRequestContext.put.paramsString = %%-csharp-fetch-option-paramsString-%%
* since: v1.47
### option: APIRequestContext.put.headers = %%-js-python-csharp-fetch-option-headers-%% ### option: APIRequestContext.put.headers = %%-js-python-csharp-fetch-option-headers-%%
* since: v1.16 * since: v1.16

View file

@ -60,7 +60,7 @@ An object with all the response HTTP headers associated with this response.
- `name` <[string]> Name of the header. - `name` <[string]> Name of the header.
- `value` <[string]> Value of the header. - `value` <[string]> Value of the header.
An array with all the request HTTP headers associated with this response. Header names are not lower-cased. An array with all the response HTTP headers associated with this response. Header names are not lower-cased.
Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times. Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times.
## async method: APIResponse.json ## async method: APIResponse.json

View file

@ -297,8 +297,10 @@ testing frameworks should explicitly create [`method: Browser.newContext`] follo
## async method: Browser.removeAllListeners ## async method: Browser.removeAllListeners
* since: v1.47 * since: v1.47
* langs: js
Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. Removes all the listeners of the given type (or all registered listeners if no type given).
Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners.
### param: Browser.removeAllListeners.type ### param: Browser.removeAllListeners.type
* since: v1.47 * since: v1.47

View file

@ -6,7 +6,7 @@ BrowserContexts provide a way to operate multiple independent browser sessions.
If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser
context. context.
Playwright allows creating "incognito" browser contexts with [`method: Browser.newContext`] method. "Incognito" browser Playwright allows creating isolated non-persistent browser contexts with [`method: Browser.newContext`] method. Non-persistent browser
contexts don't write any browsing data to disk. contexts don't write any browsing data to disk.
```js ```js
@ -415,42 +415,13 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
[`method: Page.addInitScript`] is not defined. [`method: Page.addInitScript`] is not defined.
::: :::
**Bundling**
If you have a complex script split into several files, it needs to be bundled into a single file first. We recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a commonjs module and pass [`option: path`] and [`option: arg`].
```js browser title="mocks/mockRandom.ts"
// This script can import other files.
import { defaultValue } from './defaultValue';
export default function(value?: number) {
window.Math.random = () => value ?? defaultValue;
}
```
```sh
# bundle with esbuild
esbuild mocks/mockRandom.ts --bundle --format=cjs --outfile=mocks/mockRandom.js
```
```js title="tests/example.spec.ts"
const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') };
// Passing 42 as an argument to the default export function.
await context.addInitScript({ path: mockPath }, 42);
// Make sure to pass undefined even if you do not need to pass an argument.
// This instructs Playwright to treat the file as a commonjs module.
await context.addInitScript({ path: mockPath }, undefined);
```
### param: BrowserContext.addInitScript.script ### param: BrowserContext.addInitScript.script
* since: v1.8 * since: v1.8
* langs: js * langs: js
- `script` <[function]|[string]|[Object]> - `script` <[function]|[string]|[Object]>
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the - `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
current working directory. current working directory. Optional.
- `content` ?<[string]> Raw script content. - `content` ?<[string]> Raw script content. Optional.
Script to be evaluated in all pages in the browser context. Script to be evaluated in all pages in the browser context.
@ -466,9 +437,7 @@ Script to be evaluated in all pages in the browser context.
* langs: js * langs: js
- `arg` ?<[Serializable]> - `arg` ?<[Serializable]>
Optional JSON-serializable argument to pass to [`param: script`]. Optional argument to pass to [`param: script`] (only supported when passing a function).
* When `script` is a function, the argument is passed to it directly.
* When `script` is a file path, the file is assumed to be a commonjs module. The default export, either `module.exports` or `module.exports.default`, should be a function that's going to be executed with this argument.
### param: BrowserContext.addInitScript.path ### param: BrowserContext.addInitScript.path
* since: v1.8 * since: v1.8
@ -1048,8 +1017,10 @@ Returns all open pages in the context.
## async method: BrowserContext.removeAllListeners ## async method: BrowserContext.removeAllListeners
* since: v1.47 * since: v1.47
* langs: js
Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. Removes all the listeners of the given type (or all registered listeners if no type given).
Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners.
### param: BrowserContext.removeAllListeners.type ### param: BrowserContext.removeAllListeners.type
* since: v1.47 * since: v1.47

View file

@ -866,7 +866,7 @@ await handle.SelectOptionAsync(new[] {
### option: ElementHandle.selectOption.force = %%-input-force-%% ### option: ElementHandle.selectOption.force = %%-input-force-%%
* since: v1.13 * since: v1.13
### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-%% ### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
* since: v1.8 * since: v1.8
### option: ElementHandle.selectOption.timeout = %%-input-timeout-%% ### option: ElementHandle.selectOption.timeout = %%-input-timeout-%%

View file

@ -1543,7 +1543,7 @@ await frame.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" })
### option: Frame.selectOption.force = %%-input-force-%% ### option: Frame.selectOption.force = %%-input-force-%%
* since: v1.13 * since: v1.13
### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-%% ### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
* since: v1.8 * since: v1.8
### option: Frame.selectOption.strict = %%-input-strict-%% ### option: Frame.selectOption.strict = %%-input-strict-%%

View file

@ -2055,7 +2055,7 @@ await element.SelectOptionAsync(new[] { "red", "green", "blue" });
### option: Locator.selectOption.force = %%-input-force-%% ### option: Locator.selectOption.force = %%-input-force-%%
* since: v1.14 * since: v1.14
### option: Locator.selectOption.noWaitAfter = %%-input-no-wait-after-%% ### option: Locator.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
* since: v1.14 * since: v1.14
### option: Locator.selectOption.timeout = %%-input-timeout-%% ### option: Locator.selectOption.timeout = %%-input-timeout-%%

View file

@ -619,42 +619,13 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
[`method: Page.addInitScript`] is not defined. [`method: Page.addInitScript`] is not defined.
::: :::
**Bundling**
If you have a complex script split into several files, it needs to be bundled into a single file first. We recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a commonjs module and pass [`option: path`] and [`option: arg`].
```js browser title="mocks/mockRandom.ts"
// This script can import other files.
import { defaultValue } from './defaultValue';
export default function(value?: number) {
window.Math.random = () => value ?? defaultValue;
}
```
```sh
# bundle with esbuild
esbuild mocks/mockRandom.ts --bundle --format=cjs --outfile=mocks/mockRandom.js
```
```js title="tests/example.spec.ts"
const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') };
// Passing 42 as an argument to the default export function.
await page.addInitScript({ path: mockPath }, 42);
// Make sure to pass undefined even if you do not need to pass an argument.
// This instructs Playwright to treat the file as a commonjs module.
await page.addInitScript({ path: mockPath }, undefined);
```
### param: Page.addInitScript.script ### param: Page.addInitScript.script
* since: v1.8 * since: v1.8
* langs: js * langs: js
- `script` <[function]|[string]|[Object]> - `script` <[function]|[string]|[Object]>
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the - `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
current working directory. current working directory. Optional.
- `content` ?<[string]> Raw script content. - `content` ?<[string]> Raw script content. Optional.
Script to be evaluated in the page. Script to be evaluated in the page.
@ -670,9 +641,7 @@ Script to be evaluated in all pages in the browser context.
* langs: js * langs: js
- `arg` ?<[Serializable]> - `arg` ?<[Serializable]>
Optional JSON-serializable argument to pass to [`param: script`]. Optional argument to pass to [`param: script`] (only supported when passing a function).
* When `script` is a function, the argument is passed to it directly.
* When `script` is a file path, the file is assumed to be a commonjs module. The default export, either `module.exports` or `module.exports.default`, should be a function that's going to be executed with this argument.
### param: Page.addInitScript.path ### param: Page.addInitScript.path
* since: v1.8 * since: v1.8
@ -3372,8 +3341,23 @@ By default, after calling the handler Playwright will wait until the overlay bec
## async method: Page.removeAllListeners ## async method: Page.removeAllListeners
* since: v1.47 * since: v1.47
* langs: js
Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. Removes all the listeners of the given type (or all registered listeners if no type given).
Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners.
**Usage**
```js
page.on('request', async request => {
const response = await request.response();
const body = await response.body();
console.log(body.byteLength);
});
await page.goto('https://playwright.dev', { waitUntil: 'domcontentloaded' });
// Waits for all the reported 'request' events to resolve.
await page.removeAllListeners('request', { behavior: 'wait' });
```
### param: Page.removeAllListeners.type ### param: Page.removeAllListeners.type
* since: v1.47 * since: v1.47
@ -3742,7 +3726,7 @@ await page.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" });
### option: Page.selectOption.force = %%-input-force-%% ### option: Page.selectOption.force = %%-input-force-%%
* since: v1.13 * since: v1.13
### option: Page.selectOption.noWaitAfter = %%-input-no-wait-after-%% ### option: Page.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
* since: v1.8 * since: v1.8
### option: Page.selectOption.strict = %%-input-strict-%% ### option: Page.selectOption.strict = %%-input-strict-%%

View file

@ -83,7 +83,7 @@ assertThat(page).not().hasURL("error");
``` ```
```csharp ```csharp
await Expect(Page).Not.ToHaveURL("error"); await Expect(Page).Not.ToHaveURLAsync("error");
``` ```
## async method: PageAssertions.NotToHaveTitle ## async method: PageAssertions.NotToHaveTitle
@ -271,7 +271,7 @@ expect(page).to_have_title(re.compile(r".*checkout"))
``` ```
```csharp ```csharp
await Expect(Page).ToHaveTitle("Playwright"); await Expect(Page).ToHaveTitleAsync("Playwright");
``` ```
### param: PageAssertions.toHaveTitle.titleOrRegExp ### param: PageAssertions.toHaveTitle.titleOrRegExp
@ -320,7 +320,7 @@ expect(page).to_have_url(re.compile(".*checkout"))
``` ```
```csharp ```csharp
await Expect(Page).ToHaveURL(new Regex(".*checkout")); await Expect(Page).ToHaveURLAsync(new Regex(".*checkout"));
``` ```
### param: PageAssertions.toHaveURL.urlOrRegExp ### param: PageAssertions.toHaveURL.urlOrRegExp

View file

@ -288,10 +288,15 @@ Returns the matching [Response] object, or `null` if the response was not receiv
## method: Request.serviceWorker ## method: Request.serviceWorker
* since: v1.24 * since: v1.24
* langs: js * langs: js
* deprecated: Requests made by a Service Worker are not reported in Playwright.
- returns: <[null]|[Worker]> - returns: <[null]|[Worker]>
This method will always return `null`. The Service [Worker] that is performing the request.
**Details**
This method is Chromium only. It's safe to call when using other browsers, but it will always be `null`.
Requests originated in a Service Worker do not have a [`method: Request.frame`] available.
## async method: Request.sizes ## async method: Request.sizes
* since: v1.15 * since: v1.15

View file

@ -364,7 +364,7 @@ Query parameters to be sent with the URL.
## python-fetch-option-params ## python-fetch-option-params
* langs: python * langs: python
- `params` <[Object]<[string], [string]|[float]|[boolean]>> - `params` <[Object]<[string], [string]|[float]|[boolean]>|[string]>
Query parameters to be sent with the URL. Query parameters to be sent with the URL.
@ -374,7 +374,13 @@ Query parameters to be sent with the URL.
Query parameters to be sent with the URL. Query parameters to be sent with the URL.
## java-csharp-fetch-params ## csharp-fetch-option-paramsString
* langs: csharp
- `paramsString` <[string]>
Query parameters to be sent with the URL.
## java-fetch-params
* langs: java * langs: java
- `options` ?<[RequestOptions]> - `options` ?<[RequestOptions]>
@ -769,12 +775,6 @@ Actual picture of each page will be scaled down if necessary to fit the specifie
Network proxy settings to use with this context. Defaults to none. Network proxy settings to use with this context. Defaults to none.
:::note
For Chromium on Windows the browser needs to be launched with the global proxy for this option to work. If all
contexts override the proxy, global proxy will be never used and can be any string, for example
`launch({ proxy: { server: 'http://per-context' } })`.
:::
## context-option-strict ## context-option-strict
- `strictSelectors` <[boolean]> - `strictSelectors` <[boolean]>

View file

@ -459,9 +459,13 @@ Google Chrome and Microsoft Edge respect enterprise policies, which include limi
Playwright's Firefox version matches the recent [Firefox Stable](https://www.mozilla.org/en-US/firefox/new/) build. Playwright doesn't work with the branded version of Firefox since it relies on patches. Playwright's Firefox version matches the recent [Firefox Stable](https://www.mozilla.org/en-US/firefox/new/) build. Playwright doesn't work with the branded version of Firefox since it relies on patches.
Note that availability of certain features, which depend heavily on the underlying platform, may vary between operating systems. For example, available media codecs vary substantially between Linux, macOS and Windows.
### WebKit ### WebKit
Playwright's WebKit is derived from the latest WebKit main branch sources, often before these updates are incorporated into Apple Safari and other WebKit-based browsers. This gives a lot of lead time to react on the potential browser update issues. Playwright doesn't work with the branded version of Safari since it relies on patches. Instead, you can test using the most recent WebKit build. Note that availability of certain features, which depend heavily on the underlying platform, may vary between operating systems. Playwright's WebKit is derived from the latest WebKit main branch sources, often before these updates are incorporated into Apple Safari and other WebKit-based browsers. This gives a lot of lead time to react on the potential browser update issues. Playwright doesn't work with the branded version of Safari since it relies on patches. Instead, you can test using the most recent WebKit build.
Note that availability of certain features, which depend heavily on the underlying platform, may vary between operating systems. For example, available media codecs vary substantially between Linux, macOS and Windows. While running WebKit on Linux CI is usually the most affordable option, for the closest-to-Safari experience you should run WebKit on mac, for example if you do video playback.
## Install behind a firewall or a proxy ## Install behind a firewall or a proxy

View file

@ -432,6 +432,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
# Force a non-shallow checkout, so that we can reference $GITHUB_BASE_REF.
# See https://github.com/actions/checkout for more details.
fetch-depth: 0
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 18

View file

@ -288,7 +288,7 @@ await Expect(page.GetByText("Strawberry")).ToBeVisibleAsync();
```java ```java
// Get the response from the HAR file // Get the response from the HAR file
page.routeFromHAR("./hars/fruit.har", new RouteFromHAROptions() page.routeFromHAR(Path.of("./hars/fruit.har"), new RouteFromHAROptions()
.setUrl("*/**/api/v1/fruits") .setUrl("*/**/api/v1/fruits")
.setUpdate(true) .setUpdate(true)
); );
@ -386,7 +386,7 @@ await page.ExpectByTextAsync("Playwright", new() { Exact = true }).ToBeVisibleAs
// Replay API requests from HAR. // Replay API requests from HAR.
// Either use a matching response from the HAR, // Either use a matching response from the HAR,
// or abort the request if nothing matches. // or abort the request if nothing matches.
page.routeFromHAR("./hars/fruit.har", new RouteFromHAROptions() page.routeFromHAR(Path.of("./hars/fruit.har"), new RouteFromHAROptions()
.setUrl("*/**/api/v1/fruits") .setUrl("*/**/api/v1/fruits")
.setUpdate(false) .setUpdate(false)
); );

View file

@ -147,7 +147,7 @@ const browser = await chromium.launch({
Browser browser = chromium.launch(new BrowserType.LaunchOptions() Browser browser = chromium.launch(new BrowserType.LaunchOptions()
.setProxy(new Proxy("http://myproxy.com:3128") .setProxy(new Proxy("http://myproxy.com:3128")
.setUsername('usr') .setUsername('usr')
.setPassword('pwd')); .setPassword('pwd')));
``` ```
```python async ```python async
@ -179,60 +179,48 @@ await using var browser = await BrowserType.LaunchAsync(new()
}); });
``` ```
When specifying proxy for each context individually, **Chromium on Windows** needs a hint that proxy will be set. This is done via passing a non-empty proxy server to the browser itself. Here is an example of a context-specific proxy: Its also possible to specify it per context:
```js tab=js-test title="playwright.config.ts" ```js tab=js-test title="example.spec.ts"
import { defineConfig } from '@playwright/test'; import { test, expect } from '@playwright/test';
export default defineConfig({
use: { test('should use custom proxy on a new context', async ({ browser }) => {
launchOptions: { const context = await browser.newContext({
// Browser proxy option is required for Chromium on Windows.
proxy: { server: 'per-context' }
},
proxy: { proxy: {
server: 'http://myproxy.com:3128', server: 'http://myproxy.com:3128',
} }
} });
const page = await context.newPage();
await context.close();
}); });
``` ```
```js tab=js-library ```js tab=js-library
const browser = await chromium.launch({ const browser = await chromium.launch();
// Browser proxy option is required for Chromium on Windows.
proxy: { server: 'per-context' }
});
const context = await browser.newContext({ const context = await browser.newContext({
proxy: { server: 'http://myproxy.com:3128' } proxy: { server: 'http://myproxy.com:3128' }
}); });
``` ```
```java ```java
Browser browser = chromium.launch(new BrowserType.LaunchOptions() Browser browser = chromium.launch();
// Browser proxy option is required for Chromium on Windows. BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setProxy(new Proxy("per-context")); .setProxy(new Proxy("http://myproxy.com:3128")));
BrowserContext context = chromium.launch(new Browser.NewContextOptions()
.setProxy(new Proxy("http://myproxy.com:3128"));
``` ```
```python async ```python async
# Browser proxy option is required for Chromium on Windows. browser = await chromium.launch()
browser = await chromium.launch(proxy={"server": "per-context"})
context = await browser.new_context(proxy={"server": "http://myproxy.com:3128"}) context = await browser.new_context(proxy={"server": "http://myproxy.com:3128"})
``` ```
```python sync ```python sync
# Browser proxy option is required for Chromium on Windows. browser = chromium.launch()
browser = chromium.launch(proxy={"server": "per-context"})
context = browser.new_context(proxy={"server": "http://myproxy.com:3128"}) context = browser.new_context(proxy={"server": "http://myproxy.com:3128"})
``` ```
```csharp ```csharp
var proxy = new Proxy { Server = "per-context" }; await using var browser = await BrowserType.LaunchAsync();
await using var browser = await BrowserType.LaunchAsync(new()
{
// Browser proxy option is required for Chromium on Windows.
Proxy = proxy
});
await using var context = await browser.NewContextAsync(new() await using var context = await browser.NewContextAsync(new()
{ {
Proxy = new Proxy { Server = "http://myproxy.com:3128" }, Proxy = new Proxy { Server = "http://myproxy.com:3128" },

View file

@ -4,6 +4,38 @@ title: "Release notes"
toc_max_heading_level: 2 toc_max_heading_level: 2
--- ---
## Version 1.47
### Network Tab improvements
The Network tab in the trace viewer has several nice improvements:
- filtering by asset type and URL
- better display of query string parameters
- preview of font assets
![Network tab now has filters](https://github.com/user-attachments/assets/4bd1b67d-90bd-438b-a227-00b9e86872e2)
### Miscellaneous
- The `mcr.microsoft.com/playwright/dotnet:v1.47.0` now serves a Playwright image based on Ubuntu 24.04 Noble.
To use the 22.04 jammy-based image, please use `mcr.microsoft.com/playwright/dotnet:v1.47.0-jammy` instead.
- The `:latest`/`:focal`/`:jammy` tag for Playwright Docker images is no longer being published. Pin to a specific version for better stability and reproducibility.
- TLS client certificates can now be passed from memory by passing [`option: cert`] and [`option: key`] as byte arrays instead of file paths.
- [`option: noWaitAfter`] in [`method: Locator.selectOption`] was deprecated.
- We've seen reports of WebGL in Webkit misbehaving on GitHub Actions `macos-13`. We recommend upgrading GitHub Actions to `macos-14`.
### Browser Versions
- Chromium 129.0.6668.29
- Mozilla Firefox 130.0
- WebKit 18.0
This version was also tested against the following stable channels:
- Google Chrome 128
- Microsoft Edge 128
## Version 1.46 ## Version 1.46
### TLS Client Certificates ### TLS Client Certificates

View file

@ -4,6 +4,38 @@ title: "Release notes"
toc_max_heading_level: 2 toc_max_heading_level: 2
--- ---
## Version 1.47
### Network Tab improvements
The Network tab in the trace viewer has several nice improvements:
- filtering by asset type and URL
- better display of query string parameters
- preview of font assets
![Network tab now has filters](https://github.com/user-attachments/assets/4bd1b67d-90bd-438b-a227-00b9e86872e2)
### Miscellaneous
- The `mcr.microsoft.com/playwright/java:v1.47.0` now serves a Playwright image based on Ubuntu 24.04 Noble.
To use the 22.02 jammy-based image, please use `mcr.microsoft.com/playwright/java:v1.47.0-jammy` instead.
- The `:latest`/`:focal`/`:jammy` tag for Playwright Docker images is no longer being published. Pin to a specific version for better stability and reproducibility.
- TLS client certificates can now be passed from memory by passing [`option: cert`] and [`option: key`] as byte arrays instead of file paths.
- [`option: noWaitAfter`] in [`method: Locator.selectOption`] was deprecated.
- We've seen reports of WebGL in Webkit misbehaving on GitHub Actions `macos-13`. We recommend upgrading GitHub Actions to `macos-14`.
### Browser Versions
- Chromium 129.0.6668.29
- Mozilla Firefox 130.0
- WebKit 18.0
This version was also tested against the following stable channels:
- Google Chrome 128
- Microsoft Edge 128
## Version 1.46 ## Version 1.46
### TLS Client Certificates ### TLS Client Certificates

View file

@ -6,6 +6,67 @@ toc_max_heading_level: 2
import LiteYouTube from '@site/src/components/LiteYouTube'; import LiteYouTube from '@site/src/components/LiteYouTube';
## Version 1.47
### Network Tab improvements
The Network tab in the UI mode and trace viewer has several nice improvements:
- filtering by asset type and URL
- better display of query string parameters
- preview of font assets
![Network tab now has filters](https://github.com/user-attachments/assets/4bd1b67d-90bd-438b-a227-00b9e86872e2)
### `--tsconfig` CLI option
By default, Playwright will look up the closest tsconfig for each imported file using a heuristic. You can now specify a single tsconfig file in the command line, and Playwright will use it for all imported files, not only test files:
```sh
# Pass a specific tsconfig
npx playwright test --tsconfig tsconfig.test.json
```
### [APIRequestContext] now accepts [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) and `string` as query parameters
You can now pass [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) and `string` as query parameters to [APIRequestContext]:
```ts
test('query params', async ({ request }) => {
const searchParams = new URLSearchParams();
searchParams.set('userId', 1);
const response = await request.get(
'https://jsonplaceholder.typicode.com/posts',
{
params: searchParams // or as a string: 'userId=1'
}
);
// ...
});
```
### Miscellaneous
- The `mcr.microsoft.com/playwright:v1.47.0` now serves a Playwright image based on Ubuntu 24.04 Noble.
To use the 22.04 jammy-based image, please use `mcr.microsoft.com/playwright:v1.47.0-jammy` instead.
- New option [`option: behavior`] in [`method: Page.removeAllListeners`], [`method: Browser.removeAllListeners`] and [`method: BrowserContext.removeAllListeners`] to wait for ongoing listeners to complete.
- TLS client certificates can now be passed from memory by passing [`option: cert`] and [`option: key`] as buffers instead of file paths.
- Attachments with a `text/html` content type can now be opened in a new tab in the HTML report. This is useful for including third-party reports or other HTML content in the Playwright test report and distributing it to your team.
- [`option: noWaitAfter`] in [`method: Locator.selectOption`] was deprecated.
- We've seen reports of WebGL in Webkit misbehaving on GitHub Actions `macos-13`. We recommend upgrading GitHub Actions to `macos-14`.
### Browser Versions
- Chromium 129.0.6668.29
- Mozilla Firefox 130.0
- WebKit 18.0
This version was also tested against the following stable channels:
- Google Chrome 128
- Microsoft Edge 128
## Version 1.46 ## Version 1.46
<LiteYouTube <LiteYouTube

View file

@ -4,6 +4,38 @@ title: "Release notes"
toc_max_heading_level: 2 toc_max_heading_level: 2
--- ---
## Version 1.47
### Network Tab improvements
The Network tab in the trace viewer has several nice improvements:
- filtering by asset type and URL
- better display of query string parameters
- preview of font assets
![Network tab now has filters](https://github.com/user-attachments/assets/4bd1b67d-90bd-438b-a227-00b9e86872e2)
### Miscellaneous
- The `mcr.microsoft.com/playwright/python:v1.47.0` now serves a Playwright image based on Ubuntu 24.04 Noble.
To use the 22.04 jammy-based image, please use `mcr.microsoft.com/playwright/python:v1.47.0-jammy` instead.
- The `:latest`/`:focal`/`:jammy` tag for Playwright Docker images is no longer being published. Pin to a specific version for better stability and reproducibility.
- TLS client certificates can now be passed from memory by passing [`option: cert`] and [`option: key`] as bytes instead of file paths.
- [`option: noWaitAfter`] in [`method: Locator.selectOption`] was deprecated.
- We've seen reports of WebGL in Webkit misbehaving on GitHub Actions `macos-13`. We recommend upgrading GitHub Actions to `macos-14`.
### Browser Versions
- Chromium 129.0.6668.29
- Mozilla Firefox 130.0
- WebKit 18.0
This version was also tested against the following stable channels:
- Google Chrome 128
- Microsoft Edge 128
## Version 1.46 ## Version 1.46
### TLS Client Certificates ### TLS Client Certificates

View file

@ -451,7 +451,7 @@ The title of the currently running test as passed to `test(title, testFunction)`
* since: v1.10 * since: v1.10
- type: <[Array]<[string]>> - type: <[Array]<[string]>>
The full title path starting with the project. The full title path starting with the test file name.
## property: TestInfo.workerIndex ## property: TestInfo.workerIndex
* since: v1.10 * since: v1.10

View file

@ -113,12 +113,10 @@ component is mounted using this script. It can be either a `.js`, `.ts`, `.jsx`
}> }>
<TabItem value="react"> <TabItem value="react">
```js ```js title="app.spec.tsx"
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import App from './App'; import App from './App';
test.use({ viewport: { width: 500, height: 500 } });
test('should work', async ({ mount }) => { test('should work', async ({ mount }) => {
const component = await mount(<App />); const component = await mount(<App />);
await expect(component).toContainText('Learn React'); await expect(component).toContainText('Learn React');
@ -129,18 +127,25 @@ test('should work', async ({ mount }) => {
<TabItem value="vue"> <TabItem value="vue">
```js ```js title="app.spec.ts"
import { test, expect } from '@playwright/experimental-ct-vue'; import { test, expect } from '@playwright/experimental-ct-vue';
import App from './App.vue'; import App from './App.vue';
test.use({ viewport: { width: 500, height: 500 } });
test('should work', async ({ mount }) => { test('should work', async ({ mount }) => {
const component = await mount(App); const component = await mount(App);
await expect(component).toContainText('Vite + Vue'); await expect(component).toContainText('Learn Vue');
}); });
``` ```
```js title="app.spec.tsx"
import { test, expect } from '@playwright/experimental-ct-vue';
import App from './App.vue';
test('should work', async ({ mount }) => {
const component = await mount(<App />);
await expect(component).toContainText('Learn Vue');
});
```
If using TypeScript and Vue make sure to add a `vue.d.ts` file to your project: If using TypeScript and Vue make sure to add a `vue.d.ts` file to your project:
```js ```js
@ -151,15 +156,13 @@ declare module '*.vue';
<TabItem value="svelte"> <TabItem value="svelte">
```js ```js title="app.spec.ts"
import { test, expect } from '@playwright/experimental-ct-svelte'; import { test, expect } from '@playwright/experimental-ct-svelte';
import App from './App.svelte'; import App from './App.svelte';
test.use({ viewport: { width: 500, height: 500 } });
test('should work', async ({ mount }) => { test('should work', async ({ mount }) => {
const component = await mount(App); const component = await mount(App);
await expect(component).toContainText('Vite + Svelte'); await expect(component).toContainText('Learn Svelte');
}); });
``` ```
@ -167,12 +170,10 @@ test('should work', async ({ mount }) => {
<TabItem value="solid"> <TabItem value="solid">
```js ```js title="app.spec.tsx"
import { test, expect } from '@playwright/experimental-ct-solid'; import { test, expect } from '@playwright/experimental-ct-solid';
import App from './App'; import App from './App';
test.use({ viewport: { width: 500, height: 500 } });
test('should work', async ({ mount }) => { test('should work', async ({ mount }) => {
const component = await mount(<App />); const component = await mount(<App />);
await expect(component).toContainText('Learn Solid'); await expect(component).toContainText('Learn Solid');
@ -261,7 +262,10 @@ export function InputMediaForTest(props: InputMediaForTestProps) {
Then test the component via testing the story: Then test the component via testing the story:
```js title="input-media.test.spec.tsx" ```js title="input-media.spec.tsx"
import { test, expect } from '@playwright/experimental-ct-react';
import { InputMediaForTest } from './input-media.story.tsx';
test('changes the image', async ({ mount }) => { test('changes the image', async ({ mount }) => {
let mediaSelected: string | null = null; let mediaSelected: string | null = null;
@ -313,7 +317,9 @@ Provide props to a component when mounted.
<TabItem value="react"> <TabItem value="react">
```js ```js title="component.spec.tsx"
import { test } from '@playwright/experimental-ct-react';
test('props', async ({ mount }) => { test('props', async ({ mount }) => {
const component = await mount(<Component msg="greetings" />); const component = await mount(<Component msg="greetings" />);
}); });
@ -322,7 +328,9 @@ test('props', async ({ mount }) => {
</TabItem> </TabItem>
<TabItem value="solid"> <TabItem value="solid">
```js ```js title="component.spec.tsx"
import { test } from '@playwright/experimental-ct-solid';
test('props', async ({ mount }) => { test('props', async ({ mount }) => {
const component = await mount(<Component msg="greetings" />); const component = await mount(<Component msg="greetings" />);
}); });
@ -331,7 +339,9 @@ test('props', async ({ mount }) => {
</TabItem> </TabItem>
<TabItem value="svelte"> <TabItem value="svelte">
```js ```js title="component.spec.ts"
import { test } from '@playwright/experimental-ct-svelte';
test('props', async ({ mount }) => { test('props', async ({ mount }) => {
const component = await mount(Component, { props: { msg: 'greetings' } }); const component = await mount(Component, { props: { msg: 'greetings' } });
}); });
@ -340,12 +350,23 @@ test('props', async ({ mount }) => {
</TabItem> </TabItem>
<TabItem value="vue"> <TabItem value="vue">
```js ```js title="component.spec.ts"
import { test } from '@playwright/experimental-ct-vue';
test('props', async ({ mount }) => { test('props', async ({ mount }) => {
const component = await mount(Component, { props: { msg: 'greetings' } }); const component = await mount(Component, { props: { msg: 'greetings' } });
}); });
``` ```
```js title="component.spec.tsx"
// Or alternatively, using the `jsx` style
import { test } from '@playwright/experimental-ct-vue';
test('props', async ({ mount }) => {
const component = await mount(<Component msg="greetings" />);
});
```
</TabItem> </TabItem>
</Tabs> </Tabs>
@ -366,36 +387,53 @@ Provide callbacks/events to a component when mounted.
<TabItem value="react"> <TabItem value="react">
```js ```js title="component.spec.tsx"
import { test } from '@playwright/experimental-ct-react';
test('callback', async ({ mount }) => { test('callback', async ({ mount }) => {
const component = await mount(<Component callback={() => {}} />); const component = await mount(<Component onClick={() => {}} />);
}); });
``` ```
</TabItem> </TabItem>
<TabItem value="solid"> <TabItem value="solid">
```js ```js title="component.spec.tsx"
import { test } from '@playwright/experimental-ct-solid';
test('callback', async ({ mount }) => { test('callback', async ({ mount }) => {
const component = await mount(<Component callback={() => {}} />); const component = await mount(<Component onClick={() => {}} />);
}); });
``` ```
</TabItem> </TabItem>
<TabItem value="svelte"> <TabItem value="svelte">
```js ```js title="component.spec.ts"
import { test } from '@playwright/experimental-ct-svelte';
test('event', async ({ mount }) => { test('event', async ({ mount }) => {
const component = await mount(Component, { on: { callback() {} } }); const component = await mount(Component, { on: { click() {} } });
}); });
``` ```
</TabItem> </TabItem>
<TabItem value="vue"> <TabItem value="vue">
```js ```js title="component.spec.ts"
import { test } from '@playwright/experimental-ct-vue';
test('event', async ({ mount }) => { test('event', async ({ mount }) => {
const component = await mount(Component, { on: { callback() {} } }); const component = await mount(Component, { on: { click() {} } });
});
```
```js title="component.spec.tsx"
// Or alternatively, using the `jsx` style
import { test } from '@playwright/experimental-ct-vue';
test('event', async ({ mount }) => {
const component = await mount(<Component v-on:click={() => {}} />);
}); });
``` ```
@ -419,7 +457,9 @@ Provide children/slots to a component when mounted.
<TabItem value="react"> <TabItem value="react">
```js ```js title="component.spec.tsx"
import { test } from '@playwright/experimental-ct-react';
test('children', async ({ mount }) => { test('children', async ({ mount }) => {
const component = await mount(<Component>Child</Component>); const component = await mount(<Component>Child</Component>);
}); });
@ -428,7 +468,9 @@ test('children', async ({ mount }) => {
</TabItem> </TabItem>
<TabItem value="solid"> <TabItem value="solid">
```js ```js title="component.spec.tsx"
import { test } from '@playwright/experimental-ct-solid';
test('children', async ({ mount }) => { test('children', async ({ mount }) => {
const component = await mount(<Component>Child</Component>); const component = await mount(<Component>Child</Component>);
}); });
@ -437,7 +479,9 @@ test('children', async ({ mount }) => {
</TabItem> </TabItem>
<TabItem value="svelte"> <TabItem value="svelte">
```js ```js title="component.spec.ts"
import { test } from '@playwright/experimental-ct-svelte';
test('slot', async ({ mount }) => { test('slot', async ({ mount }) => {
const component = await mount(Component, { slots: { default: 'Slot' } }); const component = await mount(Component, { slots: { default: 'Slot' } });
}); });
@ -446,12 +490,23 @@ test('slot', async ({ mount }) => {
</TabItem> </TabItem>
<TabItem value="vue"> <TabItem value="vue">
```js ```js title="component.spec.ts"
import { test } from '@playwright/experimental-ct-vue';
test('slot', async ({ mount }) => { test('slot', async ({ mount }) => {
const component = await mount(Component, { slots: { default: 'Slot' } }); const component = await mount(Component, { slots: { default: 'Slot' } });
}); });
``` ```
```js title="component.spec.tsx"
// Or alternatively, using the `jsx` style
import { test } from '@playwright/experimental-ct-vue';
test('children', async ({ mount }) => {
const component = await mount(<Component>Child</Component>);
});
```
</TabItem> </TabItem>
</Tabs> </Tabs>
@ -614,7 +669,9 @@ Unmount the mounted component from the DOM. This is useful for testing the compo
<TabItem value="react"> <TabItem value="react">
```js ```js title="component.spec.tsx"
import { test } from '@playwright/experimental-ct-react';
test('unmount', async ({ mount }) => { test('unmount', async ({ mount }) => {
const component = await mount(<Component/>); const component = await mount(<Component/>);
await component.unmount(); await component.unmount();
@ -624,7 +681,9 @@ test('unmount', async ({ mount }) => {
</TabItem> </TabItem>
<TabItem value="solid"> <TabItem value="solid">
```js ```js title="component.spec.tsx"
import { test } from '@playwright/experimental-ct-solid';
test('unmount', async ({ mount }) => { test('unmount', async ({ mount }) => {
const component = await mount(<Component/>); const component = await mount(<Component/>);
await component.unmount(); await component.unmount();
@ -634,7 +693,9 @@ test('unmount', async ({ mount }) => {
</TabItem> </TabItem>
<TabItem value="svelte"> <TabItem value="svelte">
```js ```js title="component.spec.ts"
import { test } from '@playwright/experimental-ct-svelte';
test('unmount', async ({ mount }) => { test('unmount', async ({ mount }) => {
const component = await mount(Component); const component = await mount(Component);
await component.unmount(); await component.unmount();
@ -644,13 +705,24 @@ test('unmount', async ({ mount }) => {
</TabItem> </TabItem>
<TabItem value="vue"> <TabItem value="vue">
```js ```js title="component.spec.ts"
import { test } from '@playwright/experimental-ct-vue';
test('unmount', async ({ mount }) => { test('unmount', async ({ mount }) => {
const component = await mount(Component); const component = await mount(Component);
await component.unmount(); await component.unmount();
}); });
``` ```
```js title="component.spec.tsx"
// Or alternatively, using the `jsx` style
import { test } from '@playwright/experimental-ct-vue';
test('unmount', async ({ mount }) => {
const component = await mount(<Component/>);
await component.unmount();
});
```
</TabItem> </TabItem>
</Tabs> </Tabs>
@ -671,11 +743,13 @@ Update props, slots/children, and/or events/callbacks of a mounted component. Th
<TabItem value="react"> <TabItem value="react">
```js ```js title="component.spec.tsx"
import { test } from '@playwright/experimental-ct-react';
test('update', async ({ mount }) => { test('update', async ({ mount }) => {
const component = await mount(<Component/>); const component = await mount(<Component/>);
await component.update( await component.update(
<Component msg="greetings" callback={() => {}}>Child</Component> <Component msg="greetings" onClick={() => {}}>Child</Component>
); );
}); });
``` ```
@ -683,11 +757,13 @@ test('update', async ({ mount }) => {
</TabItem> </TabItem>
<TabItem value="solid"> <TabItem value="solid">
```js ```js title="component.spec.tsx"
import { test } from '@playwright/experimental-ct-solid';
test('update', async ({ mount }) => { test('update', async ({ mount }) => {
const component = await mount(<Component/>); const component = await mount(<Component/>);
await component.update( await component.update(
<Component msg="greetings" callback={() => {}}>Child</Component> <Component msg="greetings" onClick={() => {}}>Child</Component>
); );
}); });
``` ```
@ -695,12 +771,14 @@ test('update', async ({ mount }) => {
</TabItem> </TabItem>
<TabItem value="svelte"> <TabItem value="svelte">
```js ```js title="component.spec.ts"
import { test } from '@playwright/experimental-ct-svelte';
test('update', async ({ mount }) => { test('update', async ({ mount }) => {
const component = await mount(Component); const component = await mount(Component);
await component.update({ await component.update({
props: { msg: 'greetings' }, props: { msg: 'greetings' },
on: { callback: () => {} }, on: { click() {} },
slots: { default: 'Child' } slots: { default: 'Child' }
}); });
}); });
@ -709,17 +787,31 @@ test('update', async ({ mount }) => {
</TabItem> </TabItem>
<TabItem value="vue"> <TabItem value="vue">
```js ```js title="component.spec.ts"
import { test } from '@playwright/experimental-ct-vue';
test('update', async ({ mount }) => { test('update', async ({ mount }) => {
const component = await mount(Component); const component = await mount(Component);
await component.update({ await component.update({
props: { msg: 'greetings' }, props: { msg: 'greetings' },
on: { callback: () => {} }, on: { click() {} },
slots: { default: 'Child' } slots: { default: 'Child' }
}); });
}); });
``` ```
```js title="component.spec.tsx"
// Or alternatively, using the `jsx` style
import { test } from '@playwright/experimental-ct-vue';
test('update', async ({ mount }) => {
const component = await mount(<Component/>);
await component.update(
<Component msg="greetings" v-on:click={() => {}}>Child</Component>
);
});
```
</TabItem> </TabItem>
</Tabs> </Tabs>

View file

@ -454,10 +454,6 @@ test('example test', async ({ slowFixture }) => {
## Fixtures-options ## Fixtures-options
:::note
Overriding custom fixtures in the config file has changed in version 1.18. [Learn more](./release-notes#breaking-change-custom-config-options).
:::
Playwright Test supports running multiple test projects that can be separately configured. You can use "option" fixtures to make your configuration options declarative and type-checked. Learn more about [parametrizing tests](./test-parameterize.md). Playwright Test supports running multiple test projects that can be separately configured. You can use "option" fixtures to make your configuration options declarative and type-checked. Learn more about [parametrizing tests](./test-parameterize.md).
Below we'll create a `defaultItem` option in addition to the `todoPage` fixture from other examples. This option will be set in configuration file. Note the tuple syntax and `{ option: true }` argument. Below we'll create a `defaultItem` option in addition to the `todoPage` fixture from other examples. This option will be set in configuration file. Note the tuple syntax and `{ option: true }` argument.
@ -555,6 +551,30 @@ export default defineConfig<MyOptions>({
}); });
``` ```
**Array as an option value**
If the value of your option is an array, for example `[{ name: 'Alice' }, { name: 'Bob' }]`, you'll need to wrap it into an extra array when providing the value. This is best illustrated with an example.
```js
type Person = { name: string };
const test = base.extend<{ persons: Person[] }>({
// Declare the option, default value is an empty array.
persons: [[], { option: true }],
});
// Option value is an array of persons.
const actualPersons = [{ name: 'Alice' }, { name: 'Bob' }];
test.use({
// CORRECT: Wrap the value into an array and pass the scope.
persons: [actualPersons, { scope: 'test' }],
});
test.use({
// WRONG: passing an array value directly will not work.
persons: actualPersons,
});
```
## Execution order ## Execution order
Each fixture has a setup and teardown phase separated by the `await use()` call in the fixture. Setup is executed before the fixture is used by the test/hook, and teardown is executed when the fixture will not be used by the test/hook anymore. Each fixture has a setup and teardown phase separated by the `await use()` call in the fixture. Setup is executed before the fixture is used by the test/hook, and teardown is executed when the fixture will not be used by the test/hook anymore.
@ -702,3 +722,64 @@ export const test = base.extend({
}, { title: 'my fixture' }], }, { title: 'my fixture' }],
}); });
``` ```
## Adding global beforeEach/afterEach hooks
[`method: Test.beforeEach`] and [`method: Test.afterEach`] hooks run before/after each test declared in the same file and same [`method: Test.describe`] block (if any). If you want to declare hooks that run before/after each test globally, you can declare them as auto fixtures like this:
```ts title="fixtures.ts"
import { test as base } from '@playwright/test';
export const test = base.extend<{ forEachTest: void }>({
forEachTest: [async ({ page }, use) => {
// This code runs before every test.
await page.goto('http://localhost:8000');
await use();
// This code runs after every test.
console.log('Last URL:', page.url());
}, { auto: true }], // automatically starts for every test.
});
```
And then import the fixtures in all your tests:
```ts title="mytest.spec.ts"
import { test } from './fixtures';
import { expect } from '@playwright/test';
test('basic', async ({ page }) => {
expect(page).toHaveURL('http://localhost:8000');
await page.goto('https://playwright.dev');
});
```
## Adding global beforeAll/afterAll hooks
[`method: Test.beforeAll`] and [`method: Test.afterAll`] hooks run before/after all tests declared in the same file and same [`method: Test.describe`] block (if any), once per worker process. If you want to declare hooks
that run before/after all tests in every file, you can declare them as auto fixtures with `scope: 'worker'` as follows:
```ts title="fixtures.ts"
import { test as base } from '@playwright/test';
export const test = base.extend<{}, { forEachWorker: void }>({
forEachWorker: [async ({}, use) => {
// This code runs before all the tests in the worker process.
console.log(`Starting test worker ${test.info().workerIndex}`);
await use();
// This code runs after all the tests in the worker process.
console.log(`Stopping test worker ${test.info().workerIndex}`);
}, { scope: 'worker', auto: true }], // automatically starts for every worker.
});
```
And then import the fixtures in all your tests:
```ts title="mytest.spec.ts"
import { test } from './fixtures';
import { expect } from '@playwright/test';
test('basic', async ({ }) => {
// ...
});
```
Note that the fixtures will still run once per [worker process](./test-parallel.md#worker-processes), but you don't need to redeclare them in every file.

View file

@ -80,14 +80,14 @@ By default, Playwright will look up a closest tsconfig for each imported file by
```sh ```sh
# Playwright will choose tsconfig automatically # Playwright will choose tsconfig automatically
npx playwrigh test npx playwright test
``` ```
Alternatively, you can specify a single tsconfig file to use in the command line, and Playwright will use it for all imported files, not only test files. Alternatively, you can specify a single tsconfig file to use in the command line, and Playwright will use it for all imported files, not only test files.
```sh ```sh
# Pass a specific tsconfig # Pass a specific tsconfig
npx playwrigh test --tsconfig=tsconfig.test.json npx playwright test --tsconfig=tsconfig.test.json
``` ```
## Manually compile tests with TypeScript ## Manually compile tests with TypeScript

290
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "playwright-internal", "name": "playwright-internal",
"version": "1.47.0-next", "version": "1.48.0-next",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "playwright-internal", "name": "playwright-internal",
"version": "1.47.0-next", "version": "1.48.0-next",
"license": "Apache-2.0", "license": "Apache-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@ -26,6 +26,7 @@
"@types/babel__core": "^7.20.2", "@types/babel__core": "^7.20.2",
"@types/codemirror": "^5.60.7", "@types/codemirror": "^5.60.7",
"@types/formidable": "^2.0.4", "@types/formidable": "^2.0.4",
"@types/immutable": "^3.8.7",
"@types/node": "^18.19.39", "@types/node": "^18.19.39",
"@types/react": "^18.0.12", "@types/react": "^18.0.12",
"@types/react-dom": "^18.0.5", "@types/react-dom": "^18.0.5",
@ -37,11 +38,13 @@
"@vitejs/plugin-basic-ssl": "^1.1.0", "@vitejs/plugin-basic-ssl": "^1.1.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"@zip.js/zip.js": "^2.7.29", "@zip.js/zip.js": "^2.7.29",
"ansi-styles": "^4.3.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"chromium-bidi": "^0.6.4",
"colors": "^1.4.0", "colors": "^1.4.0",
"concurrently": "^6.2.1", "concurrently": "^6.2.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.0.0", "dotenv": "^16.4.5",
"electron": "^30.1.2", "electron": "^30.1.2",
"esbuild": "^0.18.11", "esbuild": "^0.18.11",
"eslint": "^8.55.0", "eslint": "^8.55.0",
@ -50,6 +53,7 @@
"eslint-plugin-react": "^7.35.0", "eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^4.6.2",
"formidable": "^2.1.1", "formidable": "^2.1.1",
"immutable": "^4.3.7",
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"mime": "^3.0.0", "mime": "^3.0.0",
"node-stream-zip": "^1.15.0", "node-stream-zip": "^1.15.0",
@ -1831,6 +1835,16 @@
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"dev": true "dev": true
}, },
"node_modules/@types/immutable": {
"version": "3.8.7",
"resolved": "https://registry.npmjs.org/@types/immutable/-/immutable-3.8.7.tgz",
"integrity": "sha512-nsHFDX48Tl3RaP4BF47HHe5njx40Pcp+0a8CIqzJata80Fp7JzkcuGB7UhZBGjH9aA1fMEahIqvPQQNmro5YLg==",
"deprecated": "This is a stub types definition for Facebook's Immutable (https://github.com/facebook/immutable-js). Facebook's Immutable provides its own type definitions, so you don't need @types/immutable installed!",
"dev": true,
"dependencies": {
"immutable": "*"
}
},
"node_modules/@types/keyv": { "node_modules/@types/keyv": {
"version": "3.1.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
@ -2358,16 +2372,38 @@
} }
}, },
"node_modules/ansi-styles": { "node_modules/ansi-styles": {
"version": "3.2.1", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": { "dependencies": {
"color-convert": "^1.9.0" "color-convert": "^2.0.1"
}, },
"engines": { "engines": {
"node": ">=4" "node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/ansi-styles/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/ansi-styles/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/ansi-to-html": { "node_modules/ansi-to-html": {
"version": "0.7.2", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz",
@ -2801,6 +2837,17 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/chalk/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@ -2828,6 +2875,20 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/chromium-bidi": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.5.tgz",
"integrity": "sha512-RuLrmzYrxSb0s9SgpB+QN5jJucPduZQ/9SIe76MDxYJuecPW5mxMdacJ1f4EtgiV+R0p3sCkznTMvH0MPGFqjA==",
"dev": true,
"dependencies": {
"mitt": "3.0.1",
"urlpattern-polyfill": "10.0.0",
"zod": "3.23.8"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/cliui": { "node_modules/cliui": {
"version": "7.0.4", "version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@ -2927,21 +2988,6 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/concurrently/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/concurrently/node_modules/chalk": { "node_modules/concurrently/node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@ -2970,24 +3016,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/concurrently/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/concurrently/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/concurrently/node_modules/has-flag": { "node_modules/concurrently/node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -3258,6 +3286,13 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"node_modules/devtools-protocol": {
"version": "0.0.1349977",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1349977.tgz",
"integrity": "sha512-5JcwlDKinshGSm+4AVLFCkokJUAKTgjmiorNmrGgYYKix1h8Ts9/fplQeK1xg/rACYw1JlEM2PwIEvny5QswKQ==",
"dev": true,
"peer": true
},
"node_modules/dezalgo": { "node_modules/dezalgo": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
@ -3293,15 +3328,15 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.3.1", "version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/motdotla/dotenv?sponsor=1" "url": "https://dotenvx.com"
} }
}, },
"node_modules/electron": { "node_modules/electron": {
@ -3757,21 +3792,6 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/eslint/node_modules/brace-expansion": { "node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -3798,24 +3818,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/eslint/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/eslint/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/eslint/node_modules/escape-string-regexp": { "node_modules/eslint/node_modules/escape-string-regexp": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@ -4613,6 +4615,12 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immutable": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
"dev": true
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -5454,6 +5462,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"dev": true
},
"node_modules/mkdirp": { "node_modules/mkdirp": {
"version": "0.5.6", "version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@ -6820,9 +6834,9 @@
} }
}, },
"node_modules/svelte": { "node_modules/svelte": {
"version": "4.2.9", "version": "4.2.19",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.9.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz",
"integrity": "sha512-hsoB/WZGEPFXeRRLPhPrbRz67PhP6sqYgvwcAs+gWdSQSvNDw+/lTeUJSWe5h2xC97Fz/8QxAOqItwBzNJPU8w==", "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==",
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.1", "@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/sourcemap-codec": "^1.4.15",
@ -7120,6 +7134,12 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/urlpattern-polyfill": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
"integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==",
"dev": true
},
"node_modules/util-extend": { "node_modules/util-extend": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz",
@ -7738,39 +7758,6 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/wrap-ansi/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@ -7905,6 +7892,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"packages/html-reporter": { "packages/html-reporter": {
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
@ -7912,10 +7908,10 @@
} }
}, },
"packages/playwright": { "packages/playwright": {
"version": "1.47.0-next", "version": "1.48.0-next",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.47.0-next" "playwright-core": "1.48.0-next"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7929,11 +7925,11 @@
}, },
"packages/playwright-browser-chromium": { "packages/playwright-browser-chromium": {
"name": "@playwright/browser-chromium", "name": "@playwright/browser-chromium",
"version": "1.47.0-next", "version": "1.48.0-next",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.47.0-next" "playwright-core": "1.48.0-next"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -7941,11 +7937,11 @@
}, },
"packages/playwright-browser-firefox": { "packages/playwright-browser-firefox": {
"name": "@playwright/browser-firefox", "name": "@playwright/browser-firefox",
"version": "1.47.0-next", "version": "1.48.0-next",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.47.0-next" "playwright-core": "1.48.0-next"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -7953,22 +7949,22 @@
}, },
"packages/playwright-browser-webkit": { "packages/playwright-browser-webkit": {
"name": "@playwright/browser-webkit", "name": "@playwright/browser-webkit",
"version": "1.47.0-next", "version": "1.48.0-next",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.47.0-next" "playwright-core": "1.48.0-next"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"packages/playwright-chromium": { "packages/playwright-chromium": {
"version": "1.47.0-next", "version": "1.48.0-next",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.47.0-next" "playwright-core": "1.48.0-next"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7978,7 +7974,7 @@
} }
}, },
"packages/playwright-core": { "packages/playwright-core": {
"version": "1.47.0-next", "version": "1.48.0-next",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
@ -7989,11 +7985,11 @@
}, },
"packages/playwright-ct-core": { "packages/playwright-ct-core": {
"name": "@playwright/experimental-ct-core", "name": "@playwright/experimental-ct-core",
"version": "1.47.0-next", "version": "1.48.0-next",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.47.0-next", "playwright": "1.48.0-next",
"playwright-core": "1.47.0-next", "playwright-core": "1.48.0-next",
"vite": "^5.2.8" "vite": "^5.2.8"
}, },
"engines": { "engines": {
@ -8002,10 +7998,10 @@
}, },
"packages/playwright-ct-react": { "packages/playwright-ct-react": {
"name": "@playwright/experimental-ct-react", "name": "@playwright/experimental-ct-react",
"version": "1.47.0-next", "version": "1.48.0-next",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.47.0-next", "@playwright/experimental-ct-core": "1.48.0-next",
"@vitejs/plugin-react": "^4.2.1" "@vitejs/plugin-react": "^4.2.1"
}, },
"bin": { "bin": {
@ -8017,10 +8013,10 @@
}, },
"packages/playwright-ct-react17": { "packages/playwright-ct-react17": {
"name": "@playwright/experimental-ct-react17", "name": "@playwright/experimental-ct-react17",
"version": "1.47.0-next", "version": "1.48.0-next",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.47.0-next", "@playwright/experimental-ct-core": "1.48.0-next",
"@vitejs/plugin-react": "^4.2.1" "@vitejs/plugin-react": "^4.2.1"
}, },
"bin": { "bin": {
@ -8032,10 +8028,10 @@
}, },
"packages/playwright-ct-solid": { "packages/playwright-ct-solid": {
"name": "@playwright/experimental-ct-solid", "name": "@playwright/experimental-ct-solid",
"version": "1.47.0-next", "version": "1.48.0-next",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.47.0-next", "@playwright/experimental-ct-core": "1.48.0-next",
"vite-plugin-solid": "^2.7.0" "vite-plugin-solid": "^2.7.0"
}, },
"bin": { "bin": {
@ -8050,17 +8046,17 @@
}, },
"packages/playwright-ct-svelte": { "packages/playwright-ct-svelte": {
"name": "@playwright/experimental-ct-svelte", "name": "@playwright/experimental-ct-svelte",
"version": "1.47.0-next", "version": "1.48.0-next",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.47.0-next", "@playwright/experimental-ct-core": "1.48.0-next",
"@sveltejs/vite-plugin-svelte": "^3.0.1" "@sveltejs/vite-plugin-svelte": "^3.0.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
}, },
"devDependencies": { "devDependencies": {
"svelte": "^4.2.8" "svelte": "^4.2.19"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -8068,10 +8064,10 @@
}, },
"packages/playwright-ct-vue": { "packages/playwright-ct-vue": {
"name": "@playwright/experimental-ct-vue", "name": "@playwright/experimental-ct-vue",
"version": "1.47.0-next", "version": "1.48.0-next",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.47.0-next", "@playwright/experimental-ct-core": "1.48.0-next",
"@vitejs/plugin-vue": "^4.2.1" "@vitejs/plugin-vue": "^4.2.1"
}, },
"bin": { "bin": {
@ -8083,10 +8079,10 @@
}, },
"packages/playwright-ct-vue2": { "packages/playwright-ct-vue2": {
"name": "@playwright/experimental-ct-vue2", "name": "@playwright/experimental-ct-vue2",
"version": "1.47.0-next", "version": "1.48.0-next",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.47.0-next", "@playwright/experimental-ct-core": "1.48.0-next",
"@vitejs/plugin-vue2": "^2.2.0" "@vitejs/plugin-vue2": "^2.2.0"
}, },
"bin": { "bin": {
@ -8135,11 +8131,11 @@
} }
}, },
"packages/playwright-firefox": { "packages/playwright-firefox": {
"version": "1.47.0-next", "version": "1.48.0-next",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.47.0-next" "playwright-core": "1.48.0-next"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8150,10 +8146,10 @@
}, },
"packages/playwright-test": { "packages/playwright-test": {
"name": "@playwright/test", "name": "@playwright/test",
"version": "1.47.0-next", "version": "1.48.0-next",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.47.0-next" "playwright": "1.48.0-next"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8163,11 +8159,11 @@
} }
}, },
"packages/playwright-webkit": { "packages/playwright-webkit": {
"version": "1.47.0-next", "version": "1.48.0-next",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.47.0-next" "playwright-core": "1.48.0-next"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"

View file

@ -1,7 +1,7 @@
{ {
"name": "playwright-internal", "name": "playwright-internal",
"private": true, "private": true,
"version": "1.47.0-next", "version": "1.48.0-next",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",
@ -24,6 +24,7 @@
"webview2test": "playwright test --config=tests/webview2/playwright.config.ts", "webview2test": "playwright test --config=tests/webview2/playwright.config.ts",
"itest": "playwright test --config=tests/installation/playwright.config.ts", "itest": "playwright test --config=tests/installation/playwright.config.ts",
"stest": "playwright test --config=tests/stress/playwright.config.ts", "stest": "playwright test --config=tests/stress/playwright.config.ts",
"biditest": "playwright test --config=tests/bidi/playwright.config.ts",
"test-html-reporter": "playwright test --config=packages/html-reporter", "test-html-reporter": "playwright test --config=packages/html-reporter",
"test-web": "playwright test --config=packages/web", "test-web": "playwright test --config=packages/web",
"ttest": "node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright.config.ts", "ttest": "node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright.config.ts",
@ -64,6 +65,7 @@
"@types/babel__core": "^7.20.2", "@types/babel__core": "^7.20.2",
"@types/codemirror": "^5.60.7", "@types/codemirror": "^5.60.7",
"@types/formidable": "^2.0.4", "@types/formidable": "^2.0.4",
"@types/immutable": "^3.8.7",
"@types/node": "^18.19.39", "@types/node": "^18.19.39",
"@types/react": "^18.0.12", "@types/react": "^18.0.12",
"@types/react-dom": "^18.0.5", "@types/react-dom": "^18.0.5",
@ -75,11 +77,13 @@
"@vitejs/plugin-basic-ssl": "^1.1.0", "@vitejs/plugin-basic-ssl": "^1.1.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"@zip.js/zip.js": "^2.7.29", "@zip.js/zip.js": "^2.7.29",
"ansi-styles": "^4.3.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"chromium-bidi": "^0.6.4",
"colors": "^1.4.0", "colors": "^1.4.0",
"concurrently": "^6.2.1", "concurrently": "^6.2.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.0.0", "dotenv": "^16.4.5",
"electron": "^30.1.2", "electron": "^30.1.2",
"esbuild": "^0.18.11", "esbuild": "^0.18.11",
"eslint": "^8.55.0", "eslint": "^8.55.0",
@ -88,6 +92,7 @@
"eslint-plugin-react": "^7.35.0", "eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^4.6.2",
"formidable": "^2.1.1", "formidable": "^2.1.1",
"immutable": "^4.3.7",
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"mime": "^3.0.0", "mime": "^3.0.0",
"node-stream-zip": "^1.15.0", "node-stream-zip": "^1.15.0",

View file

@ -75,11 +75,16 @@ export const AttachmentLink: React.FunctionComponent<{
attachment: TestAttachment, attachment: TestAttachment,
href?: string, href?: string,
linkName?: string, linkName?: string,
}> = ({ attachment, href, linkName }) => { openInNewTab?: boolean,
}> = ({ attachment, href, linkName, openInNewTab }) => {
return <TreeItem title={<span> return <TreeItem title={<span>
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()} {attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>} {attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
{!attachment.path && <span>{linkifyText(attachment.name)}</span>} {!attachment.path && (
openInNewTab
? <a href={URL.createObjectURL(new Blob([attachment.body!], { type: attachment.contentType }))} target='_blank' rel='noreferrer' onClick={e => e.stopPropagation()}>{attachment.name}</a>
: <span>{linkifyText(attachment.name)}</span>
)}
</span>} loadChildren={attachment.body ? () => { </span>} loadChildren={attachment.body ? () => {
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>]; return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
} : undefined} depth={0} style={{ lineHeight: '32px' }}></TreeItem>; } : undefined} depth={0} style={{ lineHeight: '32px' }}></TreeItem>;

View file

@ -67,15 +67,16 @@ export const TestResultView: React.FC<{
anchor: 'video' | 'diff' | '', anchor: 'video' | 'diff' | '',
}> = ({ result, anchor }) => { }> = ({ result, anchor }) => {
const { screenshots, videos, traces, otherAttachments, diffs } = React.useMemo(() => { const { screenshots, videos, traces, otherAttachments, diffs, htmls } = React.useMemo(() => {
const attachments = result?.attachments || []; const attachments = result?.attachments || [];
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/'))); const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
const videos = attachments.filter(a => a.name === 'video'); const videos = attachments.filter(a => a.name === 'video');
const traces = attachments.filter(a => a.name === 'trace'); const traces = attachments.filter(a => a.name === 'trace');
const htmls = attachments.filter(a => a.contentType.startsWith('text/html'));
const otherAttachments = new Set<TestAttachment>(attachments); const otherAttachments = new Set<TestAttachment>(attachments);
[...screenshots, ...videos, ...traces].forEach(a => otherAttachments.delete(a)); [...screenshots, ...videos, ...traces, ...htmls].forEach(a => otherAttachments.delete(a));
const diffs = groupImageDiffs(screenshots); const diffs = groupImageDiffs(screenshots);
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs }; return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, htmls };
}, [result]); }, [result]);
const videoRef = React.useRef<HTMLDivElement>(null); const videoRef = React.useRef<HTMLDivElement>(null);
@ -135,7 +136,10 @@ export const TestResultView: React.FC<{
</div>)} </div>)}
</AutoChip>} </AutoChip>}
{!!otherAttachments.size && <AutoChip header='Attachments'> {!!(otherAttachments.size + htmls.length) && <AutoChip header='Attachments'>
{[...htmls].map((a, i) => (
<AttachmentLink key={`html-link-${i}`} attachment={a} openInNewTab />)
)}
{[...otherAttachments].map((a, i) => <AttachmentLink key={`attachment-link-${i}`} attachment={a}></AttachmentLink>)} {[...otherAttachments].map((a, i) => <AttachmentLink key={`attachment-link-${i}`} attachment={a}></AttachmentLink>)}
</AutoChip>} </AutoChip>}
</div>; </div>;

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/browser-chromium", "name": "@playwright/browser-chromium",
"version": "1.47.0-next", "version": "1.48.0-next",
"description": "Playwright package that automatically installs Chromium", "description": "Playwright package that automatically installs Chromium",
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,6 +27,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.47.0-next" "playwright-core": "1.48.0-next"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/browser-firefox", "name": "@playwright/browser-firefox",
"version": "1.47.0-next", "version": "1.48.0-next",
"description": "Playwright package that automatically installs Firefox", "description": "Playwright package that automatically installs Firefox",
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,6 +27,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.47.0-next" "playwright-core": "1.48.0-next"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/browser-webkit", "name": "@playwright/browser-webkit",
"version": "1.47.0-next", "version": "1.48.0-next",
"description": "Playwright package that automatically installs WebKit", "description": "Playwright package that automatically installs WebKit",
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,6 +27,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.47.0-next" "playwright-core": "1.48.0-next"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-chromium", "name": "playwright-chromium",
"version": "1.47.0-next", "version": "1.48.0-next",
"description": "A high-level API to automate Chromium", "description": "A high-level API to automate Chromium",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,6 +30,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.47.0-next" "playwright-core": "1.48.0-next"
} }
} }

View file

@ -16,6 +16,7 @@ This project incorporates components from the projects listed below. The origina
- concat-map@0.0.1 (https://github.com/substack/node-concat-map) - concat-map@0.0.1 (https://github.com/substack/node-concat-map)
- debug@4.3.4 (https://github.com/debug-js/debug) - debug@4.3.4 (https://github.com/debug-js/debug)
- define-lazy-prop@2.0.0 (https://github.com/sindresorhus/define-lazy-prop) - define-lazy-prop@2.0.0 (https://github.com/sindresorhus/define-lazy-prop)
- dotenv@16.4.5 (https://github.com/motdotla/dotenv)
- end-of-stream@1.4.4 (https://github.com/mafintosh/end-of-stream) - end-of-stream@1.4.4 (https://github.com/mafintosh/end-of-stream)
- escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp) - escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp)
- extract-zip@2.0.1 (https://github.com/maxogden/extract-zip) - extract-zip@2.0.1 (https://github.com/maxogden/extract-zip)
@ -472,6 +473,34 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
========================================= =========================================
END OF define-lazy-prop@2.0.0 AND INFORMATION END OF define-lazy-prop@2.0.0 AND INFORMATION
%% dotenv@16.4.5 NOTICES AND INFORMATION BEGIN HERE
=========================================
Copyright (c) 2015, Scott Motte
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
=========================================
END OF dotenv@16.4.5 AND INFORMATION
%% end-of-stream@1.4.4 NOTICES AND INFORMATION BEGIN HERE %% end-of-stream@1.4.4 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================
The MIT License (MIT) The MIT License (MIT)
@ -1514,6 +1543,6 @@ END OF yazl@2.5.1 AND INFORMATION
SUMMARY BEGIN HERE SUMMARY BEGIN HERE
========================================= =========================================
Total Packages: 45 Total Packages: 46
========================================= =========================================
END OF SUMMARY END OF SUMMARY

View file

@ -3,31 +3,31 @@
"browsers": [ "browsers": [
{ {
"name": "chromium", "name": "chromium",
"revision": "1131", "revision": "1135",
"installByDefault": true, "installByDefault": true,
"browserVersion": "128.0.6613.36" "browserVersion": "129.0.6668.42"
}, },
{ {
"name": "chromium-tip-of-tree", "name": "chromium-tip-of-tree",
"revision": "1250", "revision": "1259",
"installByDefault": false, "installByDefault": false,
"browserVersion": "129.0.6658.0" "browserVersion": "130.0.6713.0"
}, },
{ {
"name": "firefox", "name": "firefox",
"revision": "1462", "revision": "1463",
"installByDefault": true, "installByDefault": true,
"browserVersion": "129.0" "browserVersion": "130.0"
}, },
{ {
"name": "firefox-beta", "name": "firefox-beta",
"revision": "1462", "revision": "1463",
"installByDefault": false, "installByDefault": false,
"browserVersion": "130.0b2" "browserVersion": "131.0b2"
}, },
{ {
"name": "webkit", "name": "webkit",
"revision": "2062", "revision": "2073",
"installByDefault": true, "installByDefault": true,
"revisionOverrides": { "revisionOverrides": {
"mac10.14": "1446", "mac10.14": "1446",
@ -42,7 +42,11 @@
{ {
"name": "ffmpeg", "name": "ffmpeg",
"revision": "1010", "revision": "1010",
"installByDefault": true "installByDefault": true,
"revisionOverrides": {
"mac12": "1010",
"mac12-arm64": "1010"
}
}, },
{ {
"name": "android", "name": "android",

View file

@ -11,6 +11,7 @@
"colors": "1.4.0", "colors": "1.4.0",
"commander": "8.3.0", "commander": "8.3.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"dotenv": "^16.4.5",
"graceful-fs": "4.2.10", "graceful-fs": "4.2.10",
"https-proxy-agent": "5.0.0", "https-proxy-agent": "5.0.0",
"jpeg-js": "0.4.4", "jpeg-js": "0.4.4",
@ -198,6 +199,17 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dotenv": {
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/escape-string-regexp": { "node_modules/escape-string-regexp": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
@ -560,6 +572,11 @@
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="
}, },
"dotenv": {
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg=="
},
"escape-string-regexp": { "escape-string-regexp": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",

View file

@ -12,6 +12,7 @@
"colors": "1.4.0", "colors": "1.4.0",
"commander": "8.3.0", "commander": "8.3.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"dotenv": "^16.4.5",
"graceful-fs": "4.2.10", "graceful-fs": "4.2.10",
"https-proxy-agent": "5.0.0", "https-proxy-agent": "5.0.0",
"jpeg-js": "0.4.4", "jpeg-js": "0.4.4",

View file

@ -20,6 +20,9 @@ export const colors = colorsLibrary;
import debugLibrary from 'debug'; import debugLibrary from 'debug';
export const debug = debugLibrary; export const debug = debugLibrary;
import dotenvLibrary from 'dotenv';
export const dotenv = dotenvLibrary;
export { getProxyForUrl } from 'proxy-from-env'; export { getProxyForUrl } from 'proxy-from-env';
export { HttpsProxyAgent } from 'https-proxy-agent'; export { HttpsProxyAgent } from 'https-proxy-agent';

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-core", "name": "playwright-core",
"version": "1.47.0-next", "version": "1.48.0-next",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -20,7 +20,7 @@ import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import type { Command } from '../utilsBundle'; import type { Command } from '../utilsBundle';
import { program } from '../utilsBundle'; import { program, dotenv } from '../utilsBundle';
export { program } from '../utilsBundle'; export { program } from '../utilsBundle';
import { runDriver, runServer, printApiJson, launchBrowserServer } from './driver'; import { runDriver, runServer, printApiJson, launchBrowserServer } from './driver';
import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer'; import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer';
@ -348,10 +348,10 @@ type CaptureOptions = {
fullPage: boolean; fullPage: boolean;
}; };
async function launchContext(options: Options, headless: boolean, executablePath?: string): Promise<{ browser: Browser, browserName: string, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, context: BrowserContext }> { async function launchContext(options: Options, extraOptions: LaunchOptions): Promise<{ browser: Browser, browserName: string, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, context: BrowserContext }> {
validateOptions(options); validateOptions(options);
const browserType = lookupBrowserType(options); const browserType = lookupBrowserType(options);
const launchOptions: LaunchOptions = { headless, executablePath }; const launchOptions: LaunchOptions = extraOptions;
if (options.channel) if (options.channel)
launchOptions.channel = options.channel as any; launchOptions.channel = options.channel as any;
launchOptions.handleSIGINT = false; launchOptions.handleSIGINT = false;
@ -363,7 +363,7 @@ async function launchContext(options: Options, headless: boolean, executablePath
// In headful mode, use host device scale factor for things to look nice. // In headful mode, use host device scale factor for things to look nice.
// In headless, keep things the way it works in Playwright by default. // In headless, keep things the way it works in Playwright by default.
// Assume high-dpi on MacOS. TODO: this is not perfect. // Assume high-dpi on MacOS. TODO: this is not perfect.
if (!headless) if (!extraOptions.headless)
contextOptions.deviceScaleFactor = os.platform() === 'darwin' ? 2 : 1; contextOptions.deviceScaleFactor = os.platform() === 'darwin' ? 2 : 1;
// Work around the WebKit GTK scrolling issue. // Work around the WebKit GTK scrolling issue.
@ -547,7 +547,7 @@ async function openPage(context: BrowserContext, url: string | undefined): Promi
} }
async function open(options: Options, url: string | undefined, language: string) { async function open(options: Options, url: string | undefined, language: string) {
const { context, launchOptions, contextOptions } = await launchContext(options, !!process.env.PWTEST_CLI_HEADLESS, process.env.PWTEST_CLI_EXECUTABLE_PATH); const { context, launchOptions, contextOptions } = await launchContext(options, { headless: !!process.env.PWTEST_CLI_HEADLESS, executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH });
await context._enableRecorder({ await context._enableRecorder({
language, language,
launchOptions, launchOptions,
@ -560,7 +560,17 @@ async function open(options: Options, url: string | undefined, language: string)
async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) { async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) {
const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options; const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options;
const { context, launchOptions, contextOptions } = await launchContext(options, !!process.env.PWTEST_CLI_HEADLESS, process.env.PWTEST_CLI_EXECUTABLE_PATH); const tracesDir = path.join(os.tmpdir(), `recorder-trace-${Date.now()}`);
const { context, launchOptions, contextOptions } = await launchContext(options, {
headless: !!process.env.PWTEST_CLI_HEADLESS,
executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH,
tracesDir,
});
dotenv.config({ path: 'playwright.env' });
if (process.env.PW_RECORDER_IS_TRACE_VIEWER) {
await fs.promises.mkdir(tracesDir, { recursive: true });
await context.tracing.start({ name: 'trace', _live: true });
}
await context._enableRecorder({ await context._enableRecorder({
language, language,
launchOptions, launchOptions,
@ -570,7 +580,6 @@ async function codegen(options: Options & { target: string, output?: string, tes
mode: 'recording', mode: 'recording',
testIdAttributeName, testIdAttributeName,
outputFile: outputFile ? path.resolve(outputFile) : undefined, outputFile: outputFile ? path.resolve(outputFile) : undefined,
handleSIGINT: false,
}); });
await openPage(context, url); await openPage(context, url);
} }
@ -587,7 +596,7 @@ async function waitForPage(page: Page, captureOptions: CaptureOptions) {
} }
async function screenshot(options: Options, captureOptions: CaptureOptions, url: string, path: string) { async function screenshot(options: Options, captureOptions: CaptureOptions, url: string, path: string) {
const { context } = await launchContext(options, true); const { context } = await launchContext(options, { headless: true });
console.log('Navigating to ' + url); console.log('Navigating to ' + url);
const page = await openPage(context, url); const page = await openPage(context, url);
await waitForPage(page, captureOptions); await waitForPage(page, captureOptions);
@ -600,7 +609,7 @@ async function screenshot(options: Options, captureOptions: CaptureOptions, url:
async function pdf(options: Options, captureOptions: CaptureOptions, url: string, path: string) { async function pdf(options: Options, captureOptions: CaptureOptions, url: string, path: string) {
if (options.browser !== 'chromium') if (options.browser !== 'chromium')
throw new Error('PDF creation is only working with Chromium'); throw new Error('PDF creation is only working with Chromium');
const { context } = await launchContext({ ...options, browser: 'chromium' }, true); const { context } = await launchContext({ ...options, browser: 'chromium' }, { headless: true });
console.log('Navigating to ' + url); console.log('Navigating to ' + url);
const page = await openPage(context, url); const page = await openPage(context, url);
await waitForPage(page, captureOptions); await waitForPage(page, captureOptions);

View file

@ -308,7 +308,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
} }
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void> { async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void> {
const source = await evaluationScript(script, arg, arguments.length > 1); const source = await evaluationScript(script, arg);
await this._channel.addInitScript({ source }); await this._channel.addInitScript({ source });
} }
@ -481,7 +481,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
mode?: 'recording' | 'inspecting', mode?: 'recording' | 'inspecting',
testIdAttributeName?: string, testIdAttributeName?: string,
outputFile?: string, outputFile?: string,
handleSIGINT?: boolean,
}) { }) {
await this._channel.recorderSupplementEnable(params); await this._channel.recorderSupplementEnable(params);
} }

View file

@ -28,37 +28,20 @@ export function envObjectToArray(env: types.Env): { name: string, value: string
return result; return result;
} }
export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg: any, hasArg: boolean, addSourceUrl: boolean = true): Promise<string> { export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise<string> {
if (typeof fun === 'function') { if (typeof fun === 'function') {
const source = fun.toString(); const source = fun.toString();
const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg); const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg);
return `(${source})(${argString})`; return `(${source})(${argString})`;
} }
if (isString(fun)) { if (arg !== undefined)
if (arg !== undefined) throw new Error('Cannot evaluate a string with arguments');
throw new Error('Cannot evaluate a string with arguments'); if (isString(fun))
return fun; return fun;
} if (fun.content !== undefined)
if (fun.content !== undefined) {
if (arg !== undefined)
throw new Error('Cannot evaluate a string with arguments');
return fun.content; return fun.content;
}
if (fun.path !== undefined) { if (fun.path !== undefined) {
let source = await fs.promises.readFile(fun.path, 'utf8'); let source = await fs.promises.readFile(fun.path, 'utf8');
if (hasArg) {
// Assume a CJS module that has a function default export.
source = `(() => {
var exports = {}; var module = { exports };
${source}
let __pw_result__ = module.exports;
if (__pw_result__ && typeof __pw_result__ === 'object' && ('default' in __pw_result__))
__pw_result__ = __pw_result__['default'];
if (typeof __pw_result__ !== 'function')
return __pw_result__;
return __pw_result__(${JSON.stringify(arg)});
})()`;
}
if (addSourceUrl) if (addSourceUrl)
source = addSourceUrlToScript(source, fun.path); source = addSourceUrlToScript(source, fun.path);
return source; return source;

View file

@ -175,8 +175,12 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
assert(options.maxRedirects === undefined || options.maxRedirects >= 0, `'maxRedirects' must be greater than or equal to '0'`); assert(options.maxRedirects === undefined || options.maxRedirects >= 0, `'maxRedirects' must be greater than or equal to '0'`);
assert(options.maxRetries === undefined || options.maxRetries >= 0, `'maxRetries' must be greater than or equal to '0'`); assert(options.maxRetries === undefined || options.maxRetries >= 0, `'maxRetries' must be greater than or equal to '0'`);
const url = options.url !== undefined ? options.url : options.request!.url(); const url = options.url !== undefined ? options.url : options.request!.url();
const params = mapParamsToArray(options.params);
const method = options.method || options.request?.method(); const method = options.method || options.request?.method();
let encodedParams = undefined;
if (typeof options.params === 'string')
encodedParams = options.params;
else if (options.params instanceof URLSearchParams)
encodedParams = options.params.toString();
// Cannot call allHeaders() here as the request may be paused inside route handler. // Cannot call allHeaders() here as the request may be paused inside route handler.
const headersObj = options.headers || options.request?.headers(); const headersObj = options.headers || options.request?.headers();
const headers = headersObj ? headersObjectToArray(headersObj) : undefined; const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
@ -228,7 +232,8 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
}; };
const result = await this._channel.fetch({ const result = await this._channel.fetch({
url, url,
params, params: typeof options.params === 'object' ? objectToArray(options.params) : undefined,
encodedParams,
method, method,
headers, headers,
postData: postDataBuffer, postData: postDataBuffer,
@ -407,30 +412,6 @@ function objectToArray(map?: { [key: string]: any }): NameValue[] | undefined {
return result; return result;
} }
function queryStringToArray(queryString: string): NameValue[] | undefined {
const searchParams = new URLSearchParams(queryString);
return searchParamsToArray(searchParams);
}
function searchParamsToArray(searchParams: URLSearchParams): NameValue[] | undefined {
if (searchParams.size === 0)
return undefined;
const result: NameValue[] = [];
for (const [name, value] of searchParams.entries())
result.push({ name, value });
return result;
}
function mapParamsToArray(params: FetchOptions['params']): NameValue[] | undefined {
if (params instanceof URLSearchParams)
return searchParamsToArray(params);
if (typeof params === 'string')
return queryStringToArray(params);
return objectToArray(params);
}
function isFilePayload(value: any): boolean { function isFilePayload(value: any): boolean {
return typeof value === 'object' && value['name'] && value['mimeType'] && value['buffer']; return typeof value === 'object' && value['name'] && value['mimeType'] && value['buffer'];
} }

View file

@ -492,7 +492,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
} }
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
const source = await evaluationScript(script, arg, arguments.length > 1); const source = await evaluationScript(script, arg);
await this._channel.addInitScript({ source }); await this._channel.addInitScript({ source });
} }

View file

@ -26,6 +26,8 @@ import { Selectors, SelectorsOwner } from './selectors';
export class Playwright extends ChannelOwner<channels.PlaywrightChannel> { export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
readonly _android: Android; readonly _android: Android;
readonly _electron: Electron; readonly _electron: Electron;
readonly _bidiChromium: BrowserType;
readonly _bidiFirefox: BrowserType;
readonly chromium: BrowserType; readonly chromium: BrowserType;
readonly firefox: BrowserType; readonly firefox: BrowserType;
readonly webkit: BrowserType; readonly webkit: BrowserType;
@ -45,6 +47,10 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
this.webkit._playwright = this; this.webkit._playwright = this;
this._android = Android.from(initializer.android); this._android = Android.from(initializer.android);
this._electron = Electron.from(initializer.electron); this._electron = Electron.from(initializer.electron);
this._bidiChromium = BrowserType.from(initializer.bidiChromium);
this._bidiChromium._playwright = this;
this._bidiFirefox = BrowserType.from(initializer.bidiFirefox);
this._bidiFirefox._playwright = this;
this.devices = this._connection.localUtils()?.devices ?? {}; this.devices = this._connection.localUtils()?.devices ?? {};
this.selectors = new Selectors(); this.selectors = new Selectors();
this.errors = { TimeoutError }; this.errors = { TimeoutError };

View file

@ -26,7 +26,7 @@ export class Selectors implements api.Selectors {
private _registrations: channels.SelectorsRegisterParams[] = []; private _registrations: channels.SelectorsRegisterParams[] = [];
async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> { async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> {
const source = await evaluationScript(script, undefined, false, false); const source = await evaluationScript(script, undefined, false);
const params = { ...options, name, source }; const params = { ...options, name, source };
for (const channel of this._channels) for (const channel of this._channels)
await channel._channel.register(params); await channel._channel.register(params);

View file

@ -33,7 +33,7 @@ export type WaitForEventOptions = Function | { predicate?: Function, timeout?: n
export type WaitForFunctionOptions = { timeout?: number, polling?: 'raf' | number }; export type WaitForFunctionOptions = { timeout?: number, polling?: 'raf' | number };
export type SelectOption = { value?: string, label?: string, index?: number, valueOrLabel?: string }; export type SelectOption = { value?: string, label?: string, index?: number, valueOrLabel?: string };
export type SelectOptionOptions = { force?: boolean, timeout?: number, noWaitAfter?: boolean }; export type SelectOptionOptions = { force?: boolean, timeout?: number };
export type FilePayload = { name: string, mimeType: string, buffer: Buffer }; export type FilePayload = { name: string, mimeType: string, buffer: Buffer };
export type StorageState = { export type StorageState = {
cookies: channels.NetworkCookie[], cookies: channels.NetworkCookie[],

View file

@ -176,6 +176,7 @@ scheme.APIRequestContextInitializer = tObject({
}); });
scheme.APIRequestContextFetchParams = tObject({ scheme.APIRequestContextFetchParams = tObject({
url: tString, url: tString,
encodedParams: tOptional(tString),
params: tOptional(tArray(tType('NameValue'))), params: tOptional(tArray(tType('NameValue'))),
method: tOptional(tString), method: tOptional(tString),
headers: tOptional(tArray(tType('NameValue'))), headers: tOptional(tArray(tType('NameValue'))),
@ -323,6 +324,8 @@ scheme.PlaywrightInitializer = tObject({
chromium: tChannel(['BrowserType']), chromium: tChannel(['BrowserType']),
firefox: tChannel(['BrowserType']), firefox: tChannel(['BrowserType']),
webkit: tChannel(['BrowserType']), webkit: tChannel(['BrowserType']),
bidiChromium: tChannel(['BrowserType']),
bidiFirefox: tChannel(['BrowserType']),
android: tChannel(['Android']), android: tChannel(['Android']),
electron: tChannel(['Electron']), electron: tChannel(['Electron']),
utils: tOptional(tChannel(['LocalUtils'])), utils: tOptional(tChannel(['LocalUtils'])),
@ -961,7 +964,6 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({
device: tOptional(tString), device: tOptional(tString),
saveStorage: tOptional(tString), saveStorage: tOptional(tString),
outputFile: tOptional(tString), outputFile: tOptional(tString),
handleSIGINT: tOptional(tBoolean),
omitCallTracking: tOptional(tBoolean), omitCallTracking: tOptional(tBoolean),
}); });
scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({})); scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({}));
@ -1637,7 +1639,6 @@ scheme.FrameSelectOptionParams = tObject({
}))), }))),
force: tOptional(tBoolean), force: tOptional(tBoolean),
timeout: tOptional(tNumber), timeout: tOptional(tNumber),
noWaitAfter: tOptional(tBoolean),
}); });
scheme.FrameSelectOptionResult = tObject({ scheme.FrameSelectOptionResult = tObject({
values: tArray(tString), values: tArray(tString),
@ -2001,7 +2002,6 @@ scheme.ElementHandleSelectOptionParams = tObject({
}))), }))),
force: tOptional(tBoolean), force: tOptional(tBoolean),
timeout: tOptional(tNumber), timeout: tOptional(tNumber),
noWaitAfter: tOptional(tBoolean),
}); });
scheme.ElementHandleSelectOptionResult = tObject({ scheme.ElementHandleSelectOptionResult = tObject({
values: tArray(tString), values: tArray(tString),

View file

@ -16,6 +16,7 @@
[playwright.ts] [playwright.ts]
./android/ ./android/
./bidi/
./chromium/ ./chromium/
./electron/ ./electron/
./firefox/ ./firefox/

View file

@ -0,0 +1,11 @@
[*]
../../utils/
../
../isomorphic/
./third_party/
[bidiOverCdp.ts]
***
[bidiChromium.ts]
../chromium/chromiumSwitches.ts

View file

@ -0,0 +1,335 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type * as channels from '@protocol/channels';
import type { RegisteredListener } from '../../utils/eventsHelper';
import { eventsHelper } from '../../utils/eventsHelper';
import type { BrowserOptions } from '../browser';
import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext } from '../browserContext';
import type { SdkObject } from '../instrumentation';
import * as network from '../network';
import type { InitScript, Page, PageDelegate } from '../page';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';
import type { BidiSession } from './bidiConnection';
import { BidiConnection } from './bidiConnection';
import { bidiBytesValueToString } from './bidiNetworkManager';
import { BidiPage } from './bidiPage';
import * as bidi from './third_party/bidiProtocol';
export class BidiBrowser extends Browser {
private readonly _connection: BidiConnection;
readonly _browserSession: BidiSession;
private _bidiSessionInfo!: bidi.Session.NewResult;
readonly _contexts = new Map<string, BidiBrowserContext>();
readonly _bidiPages = new Map<bidi.BrowsingContext.BrowsingContext, BidiPage>();
private readonly _eventListeners: RegisteredListener[];
static async connect(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions): Promise<BidiBrowser> {
const browser = new BidiBrowser(parent, transport, options);
if ((options as any).__testHookOnConnectToBrowser)
await (options as any).__testHookOnConnectToBrowser();
let proxy: bidi.Session.ManualProxyConfiguration | undefined;
if (options.proxy) {
proxy = {
proxyType: 'manual',
};
const url = new URL(options.proxy.server); // Validate proxy server.
switch (url.protocol) {
case 'http:':
proxy.httpProxy = url.host;
break;
case 'https:':
proxy.httpsProxy = url.host;
break;
case 'socks4:':
proxy.socksProxy = url.host;
proxy.socksVersion = 4;
break;
case 'socks5:':
proxy.socksProxy = url.host;
proxy.socksVersion = 5;
break;
default:
throw new Error('Invalid proxy server protocol: ' + options.proxy.server);
}
if (options.proxy.bypass)
proxy.noProxy = options.proxy.bypass.split(',');
// TODO: support authentication.
}
browser._bidiSessionInfo = await browser._browserSession.send('session.new', {
capabilities: {
alwaysMatch: {
acceptInsecureCerts: false,
proxy,
unhandledPromptBehavior: {
default: bidi.Session.UserPromptHandlerType.Ignore,
},
webSocketUrl: true
},
}
});
await browser._browserSession.send('session.subscribe', {
events: [
'browsingContext',
'network',
'log',
'script',
],
});
return browser;
}
constructor(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions) {
super(parent, options);
this._connection = new BidiConnection(transport, this._onDisconnect.bind(this), options.protocolLogger, options.browserLogsCollector);
this._browserSession = this._connection.browserSession;
this._eventListeners = [
eventsHelper.addEventListener(this._browserSession, 'browsingContext.contextCreated', this._onBrowsingContextCreated.bind(this)),
eventsHelper.addEventListener(this._browserSession, 'script.realmDestroyed', this._onScriptRealmDestroyed.bind(this)),
];
}
_onDisconnect() {
this._didClose();
}
async doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext> {
const { userContext } = await this._browserSession.send('browser.createUserContext', {});
const context = new BidiBrowserContext(this, userContext, options);
await context._initialize();
this._contexts.set(userContext, context);
return context;
}
contexts(): BrowserContext[] {
return Array.from(this._contexts.values());
}
version(): string {
return this._bidiSessionInfo.capabilities.browserVersion;
}
userAgent(): string {
return this._bidiSessionInfo.capabilities.userAgent;
}
isConnected(): boolean {
return !this._connection.isClosed();
}
private _onBrowsingContextCreated(event: bidi.BrowsingContext.Info) {
if (event.parent) {
const parentFrameId = event.parent;
for (const page of this._bidiPages.values()) {
const parentFrame = page._page._frameManager.frame(parentFrameId);
if (!parentFrame)
continue;
page._session.addFrameBrowsingContext(event.context);
page._page._frameManager.frameAttached(event.context, parentFrameId);
return;
}
return;
}
let context = this._contexts.get(event.userContext);
if (!context)
context = this._defaultContext as BidiBrowserContext;
if (!context)
return;
const session = this._connection.createMainFrameBrowsingContextSession(event.context);
const opener = event.originalOpener && this._bidiPages.get(event.originalOpener);
const page = new BidiPage(context, session, opener || null);
this._bidiPages.set(event.context, page);
}
_onBrowsingContextDestroyed(event: bidi.BrowsingContext.Info) {
if (event.parent) {
this._browserSession.removeFrameBrowsingContext(event.context);
const parentFrameId = event.parent;
for (const page of this._bidiPages.values()) {
const parentFrame = page._page._frameManager.frame(parentFrameId);
if (!parentFrame)
continue;
page._page._frameManager.frameDetached(event.context);
return;
}
return;
}
const bidiPage = this._bidiPages.get(event.context);
if (!bidiPage)
return;
bidiPage.didClose();
this._bidiPages.delete(event.context);
}
private _onScriptRealmDestroyed(event: bidi.Script.RealmDestroyedParameters) {
for (const page of this._bidiPages.values()) {
if (page._onRealmDestroyed(event))
return;
}
}
}
export class BidiBrowserContext extends BrowserContext {
declare readonly _browser: BidiBrowser;
constructor(browser: BidiBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) {
super(browser, options, browserContextId);
this._authenticateProxyViaHeader();
}
private _bidiPages() {
return [...this._browser._bidiPages.values()].filter(bidiPage => bidiPage._browserContext === this);
}
pages(): Page[] {
return this._bidiPages().map(bidiPage => bidiPage._initializedPage).filter(Boolean) as Page[];
}
async newPageDelegate(): Promise<PageDelegate> {
assertBrowserContextIsNotOwned(this);
const { context } = await this._browser._browserSession.send('browsingContext.create', {
type: bidi.BrowsingContext.CreateType.Window,
userContext: this._browserContextId,
});
return this._browser._bidiPages.get(context)!;
}
async doGetCookies(urls: string[]): Promise<channels.NetworkCookie[]> {
const { cookies } = await this._browser._browserSession.send('storage.getCookies',
{ partition: { type: 'storageKey', userContext: this._browserContextId } });
return network.filterCookies(cookies.map((c: bidi.Network.Cookie) => {
const copy: channels.NetworkCookie = {
name: c.name,
value: bidiBytesValueToString(c.value),
domain: c.domain,
path: c.path,
httpOnly: c.httpOnly,
secure: c.secure,
expires: c.expiry ?? -1,
sameSite: c.sameSite ? fromBidiSameSite(c.sameSite) : 'None',
};
return copy;
}), urls);
}
async addCookies(cookies: channels.SetNetworkCookie[]) {
cookies = network.rewriteCookies(cookies);
const promises = cookies.map((c: channels.SetNetworkCookie) => {
const cookie: bidi.Storage.PartialCookie = {
name: c.name,
value: { type: 'string', value: c.value },
domain: c.domain!,
path: c.path,
httpOnly: c.httpOnly,
secure: c.secure,
sameSite: c.sameSite && toBidiSameSite(c.sameSite),
expiry: (c.expires === -1 || c.expires === undefined) ? undefined : Math.round(c.expires),
};
return this._browser._browserSession.send('storage.setCookie',
{ cookie, partition: { type: 'storageKey', userContext: this._browserContextId } });
});
await Promise.all(promises);
}
async doClearCookies() {
await this._browser._browserSession.send('storage.deleteCookies',
{ partition: { type: 'storageKey', userContext: this._browserContextId } });
}
async doGrantPermissions(origin: string, permissions: string[]) {
}
async doClearPermissions() {
}
async setGeolocation(geolocation?: types.Geolocation): Promise<void> {
}
async setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void> {
}
async setUserAgent(userAgent: string | undefined): Promise<void> {
}
async setOffline(offline: boolean): Promise<void> {
}
async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void> {
this._options.httpCredentials = httpCredentials;
for (const page of this.pages())
await (page._delegate as BidiPage).updateHttpCredentials();
}
async doAddInitScript(initScript: InitScript) {
await Promise.all(this.pages().map(page => (page._delegate as BidiPage).addInitScript(initScript)));
}
async doRemoveNonInternalInitScripts() {
}
async doUpdateRequestInterception(): Promise<void> {
}
onClosePersistent() {}
override async clearCache(): Promise<void> {
}
async doClose(reason: string | undefined) {
// TODO: implement for persistent context
if (!this._browserContextId)
return;
await this._browser._browserSession.send('browser.removeUserContext', {
userContext: this._browserContextId
});
this._browser._contexts.delete(this._browserContextId);
}
async cancelDownload(uuid: string) {
}
}
function fromBidiSameSite(sameSite: bidi.Network.SameSite): channels.NetworkCookie['sameSite'] {
switch (sameSite) {
case 'strict': return 'Strict';
case 'lax': return 'Lax';
case 'none': return 'None';
}
return 'None';
}
function toBidiSameSite(sameSite: channels.SetNetworkCookie['sameSite']): bidi.Network.SameSite {
switch (sameSite) {
case 'Strict': return bidi.Network.SameSite.Strict;
case 'Lax': return bidi.Network.SameSite.Lax;
case 'None': return bidi.Network.SameSite.None;
}
return bidi.Network.SameSite.None;
}
export namespace Network {
export const enum SameSite {
Strict = 'strict',
Lax = 'lax',
None = 'none',
}
}

View file

@ -0,0 +1,161 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import os from 'os';
import { assert, wrapInASCIIBox } from '../../utils';
import type { Env } from '../../utils/processLauncher';
import type { BrowserOptions } from '../browser';
import { BrowserReadyState, BrowserType, kNoXServerRunningError } from '../browserType';
import { chromiumSwitches } from '../chromium/chromiumSwitches';
import type { SdkObject } from '../instrumentation';
import type { ProtocolError } from '../protocolError';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';
import { BidiBrowser } from './bidiBrowser';
import { kBrowserCloseMessageId } from './bidiConnection';
export class BidiChromium extends BrowserType {
constructor(parent: SdkObject) {
super(parent, 'bidi');
this._useBidi = true;
}
override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BidiBrowser> {
// Chrome doesn't support Bidi, we create Bidi over CDP which is used by Chrome driver.
// bidiOverCdp depends on chromium-bidi which we only have in devDependencies, so
// we load bidiOverCdp dynamically.
const bidiTransport = await require('./bidiOverCdp').connectBidiOverCdp(transport);
(transport as any)[kBidiOverCdpWrapper] = bidiTransport;
return BidiBrowser.connect(this.attribution.playwright, bidiTransport, options);
}
override doRewriteStartupLog(error: ProtocolError): ProtocolError {
if (!error.logs)
return error;
if (error.logs.includes('Missing X server'))
error.logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
// These error messages are taken from Chromium source code as of July, 2020:
// https://github.com/chromium/chromium/blob/70565f67e79f79e17663ad1337dc6e63ee207ce9/content/browser/zygote_host/zygote_host_impl_linux.cc
if (!error.logs.includes('crbug.com/357670') && !error.logs.includes('No usable sandbox!') && !error.logs.includes('crbug.com/638180'))
return error;
error.logs = [
`Chromium sandboxing failed!`,
`================================`,
`To avoid the sandboxing issue, do either of the following:`,
` - (preferred): Configure your environment to support sandboxing`,
` - (alternative): Launch Chromium without sandbox using 'chromiumSandbox: false' option`,
`================================`,
``,
].join('\n');
return error;
}
override amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
return env;
}
override attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
const bidiTransport = (transport as any)[kBidiOverCdpWrapper];
if (bidiTransport)
transport = bidiTransport;
transport.send({ method: 'browser.close', params: {}, id: kBrowserCloseMessageId });
}
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
const chromeArguments = this._innerDefaultArgs(options);
chromeArguments.push(`--user-data-dir=${userDataDir}`);
chromeArguments.push('--remote-debugging-port=0');
if (isPersistent)
chromeArguments.push('about:blank');
else
chromeArguments.push('--no-startup-window');
return chromeArguments;
}
override readyState(options: types.LaunchOptions): BrowserReadyState | undefined {
assert(options.useWebSocket);
return new ChromiumReadyState();
}
private _innerDefaultArgs(options: types.LaunchOptions): string[] {
const { args = [], proxy } = options;
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
if (userDataDirArg)
throw this._createUserDataDirArgMisuseError('--user-data-dir');
if (args.find(arg => arg.startsWith('--remote-debugging-pipe')))
throw new Error('Playwright manages remote debugging connection itself.');
if (args.find(arg => !arg.startsWith('-')))
throw new Error('Arguments can not specify page to be opened');
const chromeArguments = [...chromiumSwitches];
if (os.platform() === 'darwin') {
// See https://github.com/microsoft/playwright/issues/7362
chromeArguments.push('--enable-use-zoom-for-dsf=false');
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1407025.
if (options.headless)
chromeArguments.push('--use-angle');
}
if (options.devtools)
chromeArguments.push('--auto-open-devtools-for-tabs');
if (options.headless) {
if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW)
chromeArguments.push('--headless=new');
else
chromeArguments.push('--headless=old');
chromeArguments.push(
'--hide-scrollbars',
'--mute-audio',
'--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4',
);
}
if (options.chromiumSandbox !== true)
chromeArguments.push('--no-sandbox');
if (proxy) {
const proxyURL = new URL(proxy.server);
const isSocks = proxyURL.protocol === 'socks5:';
// https://www.chromium.org/developers/design-documents/network-settings
if (isSocks && !this.attribution.playwright.options.socksProxyPort) {
// https://www.chromium.org/developers/design-documents/network-stack/socks-proxy
chromeArguments.push(`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"`);
}
chromeArguments.push(`--proxy-server=${proxy.server}`);
const proxyBypassRules = [];
// https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
if (this.attribution.playwright.options.socksProxyPort)
proxyBypassRules.push('<-loopback>');
if (proxy.bypass)
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>'))
proxyBypassRules.push('<-loopback>');
if (proxyBypassRules.length > 0)
chromeArguments.push(`--proxy-bypass-list=${proxyBypassRules.join(';')}`);
}
chromeArguments.push(...args);
return chromeArguments;
}
}
class ChromiumReadyState extends BrowserReadyState {
override onBrowserOutput(message: string): void {
const match = message.match(/DevTools listening on (.*)/);
if (match)
this._wsEndpoint.resolve(match[1]);
}
}
const kBidiOverCdpWrapper = Symbol('kBidiConnectionWrapper');

View file

@ -0,0 +1,232 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter } from 'events';
import { assert } from '../../utils';
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import type { RecentLogsCollector } from '../../utils/debugLogger';
import { debugLogger } from '../../utils/debugLogger';
import type { ProtocolLogger } from '../types';
import { helper } from '../helper';
import { ProtocolError } from '../protocolError';
import type * as bidi from './third_party/bidiProtocol';
import type * as bidiCommands from './third_party/bidiCommands';
// BidiPlaywright uses this special id to issue Browser.close command which we
// should ignore.
export const kBrowserCloseMessageId = 0;
export class BidiConnection {
private readonly _transport: ConnectionTransport;
private readonly _onDisconnect: () => void;
private readonly _protocolLogger: ProtocolLogger;
private readonly _browserLogsCollector: RecentLogsCollector;
_browserDisconnectedLogs: string | undefined;
private _lastId = 0;
private _closed = false;
readonly browserSession: BidiSession;
readonly _browsingContextToSession = new Map<string, BidiSession>();
constructor(transport: ConnectionTransport, onDisconnect: () => void, protocolLogger: ProtocolLogger, browserLogsCollector: RecentLogsCollector) {
this._transport = transport;
this._onDisconnect = onDisconnect;
this._protocolLogger = protocolLogger;
this._browserLogsCollector = browserLogsCollector;
this.browserSession = new BidiSession(this, '', (message: any) => {
this.rawSend(message);
});
this._transport.onmessage = this._dispatchMessage.bind(this);
// onclose should be set last, since it can be immediately called.
this._transport.onclose = this._onClose.bind(this);
}
nextMessageId(): number {
return ++this._lastId;
}
rawSend(message: ProtocolRequest) {
this._protocolLogger('send', message);
this._transport.send(message);
}
private _dispatchMessage(message: ProtocolResponse) {
this._protocolLogger('receive', message);
const object = message as bidi.Message;
// Bidi messages do not have a common session identifier, so we
// route them based on BrowsingContext.
if (object.type === 'event') {
// Route page events to the right session.
let context;
if ('context' in object.params)
context = object.params.context;
else if (object.method === 'log.entryAdded')
context = object.params.source?.context;
if (context) {
const session = this._browsingContextToSession.get(context);
if (session) {
session.dispatchMessage(message);
return;
}
}
} else if (message.id) {
// Find caller session.
for (const session of this._browsingContextToSession.values()) {
if (session.hasCallback(message.id)) {
session.dispatchMessage(message);
return;
}
}
}
this.browserSession.dispatchMessage(message);
}
_onClose(reason?: string) {
this._closed = true;
this._transport.onmessage = undefined;
this._transport.onclose = undefined;
this._browserDisconnectedLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs(), reason);
this.browserSession.dispose();
this._onDisconnect();
}
isClosed() {
return this._closed;
}
close() {
if (!this._closed)
this._transport.close();
}
createMainFrameBrowsingContextSession(bowsingContextId: bidi.BrowsingContext.BrowsingContext): BidiSession {
const result = new BidiSession(this, bowsingContextId, message => this.rawSend(message));
this._browsingContextToSession.set(bowsingContextId, result);
return result;
}
}
type BidiEvents = {
[K in bidi.Event['method']]: Extract<bidi.Event, {method: K}>;
};
export class BidiSession extends EventEmitter {
readonly connection: BidiConnection;
readonly sessionId: string;
private _disposed = false;
private readonly _rawSend: (message: any) => void;
private readonly _callbacks = new Map<number, { resolve: (o: any) => void, reject: (e: ProtocolError) => void, error: ProtocolError }>();
private _crashed: boolean = false;
private readonly _browsingContexts = new Set<string>();
override on: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
override addListener: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
override off: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
override removeListener: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
override once: <T extends keyof BidiEvents | symbol>(event: T, listener: (payload: T extends symbol ? any : BidiEvents[T extends keyof BidiEvents ? T : never]['params']) => void) => this;
constructor(connection: BidiConnection, sessionId: string, rawSend: (message: any) => void) {
super();
this.setMaxListeners(0);
this.connection = connection;
this.sessionId = sessionId;
this._rawSend = rawSend;
this.on = super.on;
this.off = super.removeListener;
this.addListener = super.addListener;
this.removeListener = super.removeListener;
this.once = super.once;
}
addFrameBrowsingContext(context: string) {
this._browsingContexts.add(context);
this.connection._browsingContextToSession.set(context, this);
}
removeFrameBrowsingContext(context: string) {
this._browsingContexts.delete(context);
this.connection._browsingContextToSession.delete(context);
}
async send<T extends keyof bidiCommands.Commands>(
method: T,
params?: bidiCommands.Commands[T]['params']
): Promise<bidiCommands.Commands[T]['returnType']> {
if (this._crashed || this._disposed || this.connection._browserDisconnectedLogs)
throw new ProtocolError(this._crashed ? 'crashed' : 'closed', undefined, this.connection._browserDisconnectedLogs);
const id = this.connection.nextMessageId();
const messageObj = { id, method, params };
this._rawSend(messageObj);
return new Promise<bidiCommands.Commands[T]['returnType']>((resolve, reject) => {
this._callbacks.set(id, { resolve, reject, error: new ProtocolError('error', method) });
});
}
sendMayFail<T extends keyof bidiCommands.Commands>(method: T, params?: bidiCommands.Commands[T]['params']): Promise<bidiCommands.Commands[T]['returnType'] | void> {
return this.send(method, params).catch(error => debugLogger.log('error', error));
}
markAsCrashed() {
this._crashed = true;
}
isDisposed(): boolean {
return this._disposed;
}
dispose() {
this._disposed = true;
this.connection._browsingContextToSession.delete(this.sessionId);
for (const context of this._browsingContexts)
this.connection._browsingContextToSession.delete(context);
this._browsingContexts.clear();
for (const callback of this._callbacks.values()) {
callback.error.type = this._crashed ? 'crashed' : 'closed';
callback.error.logs = this.connection._browserDisconnectedLogs;
callback.reject(callback.error);
}
this._callbacks.clear();
}
hasCallback(id: number): boolean {
return this._callbacks.has(id);
}
dispatchMessage(message: any) {
const object = message as bidi.Message;
if (object.id === kBrowserCloseMessageId)
return;
if (object.id && this._callbacks.has(object.id)) {
const callback = this._callbacks.get(object.id)!;
this._callbacks.delete(object.id);
if (object.type === 'error') {
callback.error.setMessage(object.error + '\nMessage: ' + object.message);
callback.reject(callback.error);
} else if (object.type === 'success') {
callback.resolve(object.result);
} else {
callback.error.setMessage('Internal error, unexpected response type: ' + JSON.stringify(object));
callback.reject(callback.error);
}
} else if (object.id) {
// Response might come after session has been disposed and rejected all callbacks.
assert(this.isDisposed());
} else {
Promise.resolve().then(() => this.emit(object.method, object.params));
}
}
}

View file

@ -0,0 +1,167 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers';
import * as js from '../javascript';
import type { BidiSession } from './bidiConnection';
import { BidiDeserializer } from './third_party/bidiDeserializer';
import * as bidi from './third_party/bidiProtocol';
import { BidiSerializer } from './third_party/bidiSerializer';
export class BidiExecutionContext implements js.ExecutionContextDelegate {
private readonly _session: BidiSession;
private readonly _target: bidi.Script.Target;
constructor(session: BidiSession, realmInfo: bidi.Script.RealmInfo) {
this._session = session;
if (realmInfo.type === 'window') {
// Simple realm does not seem to work for Window contexts.
this._target = {
context: realmInfo.context,
sandbox: realmInfo.sandbox,
};
} else {
this._target = {
realm: realmInfo.realm
};
}
}
async rawEvaluateJSON(expression: string): Promise<any> {
const response = await this._session.send('script.evaluate', {
expression,
target: this._target,
serializationOptions: {
maxObjectDepth: 10,
maxDomDepth: 10,
},
awaitPromise: true,
userActivation: true,
});
if (response.type === 'success')
return BidiDeserializer.deserialize(response.result);
if (response.type === 'exception')
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
}
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> {
const response = await this._session.send('script.evaluate', {
expression,
target: this._target,
resultOwnership: bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned.
serializationOptions: { maxObjectDepth: 0, maxDomDepth: 0 },
awaitPromise: true,
userActivation: true,
});
if (response.type === 'success') {
if ('handle' in response.result)
return response.result.handle!;
throw new js.JavaScriptErrorInEvaluate('Cannot get handle: ' + JSON.stringify(response.result));
}
if (response.type === 'exception')
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
}
rawCallFunctionNoReply(func: Function, ...args: any[]) {
throw new Error('Method not implemented.');
}
async evaluateWithArguments(functionDeclaration: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
const response = await this._session.send('script.callFunction', {
functionDeclaration,
target: this._target,
arguments: [
{ handle: utilityScript._objectId! },
...values.map(BidiSerializer.serialize),
...objectIds.map(handle => ({ handle })),
],
resultOwnership: returnByValue ? undefined : bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned.
serializationOptions: returnByValue ? {} : { maxObjectDepth: 0, maxDomDepth: 0 },
awaitPromise: true,
userActivation: true,
});
if (response.type === 'exception')
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
if (response.type === 'success') {
if (returnByValue)
return parseEvaluationResultValue(BidiDeserializer.deserialize(response.result));
const objectId = 'handle' in response.result ? response.result.handle : undefined ;
return utilityScript._context.createHandle({ objectId, ...response.result });
}
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
}
async getProperties(context: js.ExecutionContext, objectId: js.ObjectId): Promise<Map<string, js.JSHandle>> {
throw new Error('Method not implemented.');
}
createHandle(context: js.ExecutionContext, jsRemoteObject: js.RemoteObject): js.JSHandle {
const remoteObject: bidi.Script.RemoteValue = jsRemoteObject as bidi.Script.RemoteValue;
return new js.JSHandle(context, remoteObject.type, renderPreview(remoteObject), jsRemoteObject.objectId, remoteObjectValue(remoteObject));
}
async releaseHandle(objectId: js.ObjectId): Promise<void> {
await this._session.send('script.disown', {
target: this._target,
handles: [objectId],
});
}
objectCount(objectId: js.ObjectId): Promise<number> {
throw new Error('Method not implemented.');
}
async rawCallFunction(functionDeclaration: string, arg: bidi.Script.LocalValue): Promise<bidi.Script.RemoteValue> {
const response = await this._session.send('script.callFunction', {
functionDeclaration,
target: this._target,
arguments: [arg],
resultOwnership: bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned.
serializationOptions: { maxObjectDepth: 0, maxDomDepth: 0 },
awaitPromise: true,
userActivation: true,
});
if (response.type === 'exception')
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
if (response.type === 'success')
return response.result;
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
}
}
function renderPreview(remoteObject: bidi.Script.RemoteValue): string | undefined {
if (remoteObject.type === 'undefined')
return 'undefined';
if (remoteObject.type === 'null')
return 'null';
if ('value' in remoteObject)
return String(remoteObject.value);
return `<${remoteObject.type}>`;
}
function remoteObjectValue(remoteObject: bidi.Script.RemoteValue): any {
if (remoteObject.type === 'undefined')
return undefined;
if (remoteObject.type === 'null')
return null;
if (remoteObject.type === 'number' && typeof remoteObject.value === 'string')
return js.parseUnserializableValue(remoteObject.value);
if ('value' in remoteObject)
return remoteObject.value;
return undefined;
}

View file

@ -0,0 +1,101 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import os from 'os';
import path from 'path';
import { assert, wrapInASCIIBox } from '../../utils';
import type { Env } from '../../utils/processLauncher';
import type { BrowserOptions } from '../browser';
import { BrowserReadyState, BrowserType, kNoXServerRunningError } from '../browserType';
import type { SdkObject } from '../instrumentation';
import type { ProtocolError } from '../protocolError';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';
import { BidiBrowser } from './bidiBrowser';
import { kBrowserCloseMessageId } from './bidiConnection';
export class BidiFirefox extends BrowserType {
constructor(parent: SdkObject) {
super(parent, 'bidi');
this._useBidi = true;
}
override async connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BidiBrowser> {
return BidiBrowser.connect(this.attribution.playwright, transport, options);
}
override doRewriteStartupLog(error: ProtocolError): ProtocolError {
if (!error.logs)
return error;
// https://github.com/microsoft/playwright/issues/6500
if (error.logs.includes(`as root in a regular user's session is not supported.`))
error.logs = '\n' + wrapInASCIIBox(`Firefox is unable to launch if the $HOME folder isn't owned by the current user.\nWorkaround: Set the HOME=/root environment variable${process.env.GITHUB_ACTION ? ' in your GitHub Actions workflow file' : ''} when running Playwright.`, 1);
if (error.logs.includes('no DISPLAY environment variable specified'))
error.logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
return error;
}
override amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
if (!path.isAbsolute(os.homedir()))
throw new Error(`Cannot launch Firefox with relative home directory. Did you set ${os.platform() === 'win32' ? 'USERPROFILE' : 'HOME'} to a relative path?`);
if (os.platform() === 'linux') {
// Always remove SNAP_NAME and SNAP_INSTANCE_NAME env variables since they
// confuse Firefox: in our case, builds never come from SNAP.
// See https://github.com/microsoft/playwright/issues/20555
return { ...env, SNAP_NAME: undefined, SNAP_INSTANCE_NAME: undefined };
}
return env;
}
override attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
transport.send({ method: 'browser.close', params: {}, id: kBrowserCloseMessageId });
}
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
const { args = [], headless } = options;
const userDataDirArg = args.find(arg => arg.startsWith('-profile') || arg.startsWith('--profile'));
if (userDataDirArg)
throw this._createUserDataDirArgMisuseError('--profile');
const firefoxArguments = ['--remote-debugging-port=0'];
if (headless)
firefoxArguments.push('--headless');
else
firefoxArguments.push('--foreground');
firefoxArguments.push(`--profile`, userDataDir);
firefoxArguments.push(...args);
// TODO: make ephemeral context work without this argument.
firefoxArguments.push('about:blank');
// if (isPersistent)
// firefoxArguments.push('about:blank');
// else
// firefoxArguments.push('-silent');
return firefoxArguments;
}
override readyState(options: types.LaunchOptions): BrowserReadyState | undefined {
assert(options.useWebSocket);
return new FirefoxReadyState();
}
}
class FirefoxReadyState extends BrowserReadyState {
override onBrowserOutput(message: string): void {
// Bidi WebSocket in Firefox.
const match = message.match(/WebDriver BiDi listening on (ws:\/\/.*)$/);
if (match)
this._wsEndpoint.resolve(match[1] + '/session');
}
}

View file

@ -0,0 +1,144 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type * as input from '../input';
import type * as types from '../types';
import type { BidiSession } from './bidiConnection';
import * as bidi from './third_party/bidiProtocol';
import { getBidiKeyValue } from './third_party/bidiKeyboard';
export class RawKeyboardImpl implements input.RawKeyboard {
private _session: BidiSession;
constructor(session: BidiSession) {
this._session = session;
}
setSession(session: BidiSession) {
this._session = session;
}
async keydown(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise<void> {
const actions: bidi.Input.KeySourceAction[] = [];
actions.push({ type: 'keyDown', value: getBidiKeyValue(key) });
// TODO: add modifiers?
await this._performActions(actions);
}
async keyup(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number): Promise<void> {
const actions: bidi.Input.KeySourceAction[] = [];
actions.push({ type: 'keyUp', value: getBidiKeyValue(key) });
await this._performActions(actions);
}
async sendText(text: string): Promise<void> {
const actions: bidi.Input.KeySourceAction[] = [];
for (const char of text) {
const value = getBidiKeyValue(char);
actions.push({ type: 'keyDown', value });
actions.push({ type: 'keyUp', value });
}
await this._performActions(actions);
}
private async _performActions(actions: bidi.Input.KeySourceAction[]) {
await this._session.send('input.performActions', {
context: this._session.sessionId,
actions: [
{
type: 'key',
id: 'pw_keyboard',
actions,
}
]
});
}
}
export class RawMouseImpl implements input.RawMouse {
private readonly _session: BidiSession;
constructor(session: BidiSession) {
this._session = session;
}
async move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, forClick: boolean): Promise<void> {
// Bidi throws when x/y are not integers.
x = Math.round(x);
y = Math.round(y);
await this._performActions([{ type: 'pointerMove', x, y }]);
}
async down(x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void> {
await this._performActions([{ type: 'pointerDown', button: toBidiButton(button) }]);
}
async up(x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void> {
await this._performActions([{ type: 'pointerUp', button: toBidiButton(button) }]);
}
async wheel(x: number, y: number, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, deltaX: number, deltaY: number): Promise<void> {
// Bidi throws when x/y are not integers.
x = Math.round(x);
y = Math.round(y);
await this._session.send('input.performActions', {
context: this._session.sessionId,
actions: [
{
type: 'wheel',
id: 'pw_mouse_wheel',
actions: [{ type: 'scroll', x, y, deltaX, deltaY }],
}
]
});
}
private async _performActions(actions: bidi.Input.PointerSourceAction[]) {
await this._session.send('input.performActions', {
context: this._session.sessionId,
actions: [
{
type: 'pointer',
id: 'pw_mouse',
parameters: {
pointerType: bidi.Input.PointerType.Mouse,
},
actions,
}
]
});
}
}
export class RawTouchscreenImpl implements input.RawTouchscreen {
private readonly _session: BidiSession;
constructor(session: BidiSession) {
this._session = session;
}
async tap(x: number, y: number, modifiers: Set<types.KeyboardModifier>) {
}
}
function toBidiButton(button: string): number {
switch (button) {
case 'left': return 0;
case 'right': return 2;
case 'middle': return 1;
}
throw new Error('Unknown button: ' + button);
}

View file

@ -0,0 +1,316 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { RegisteredListener } from '../../utils/eventsHelper';
import { eventsHelper } from '../../utils/eventsHelper';
import type { Page } from '../page';
import * as network from '../network';
import type * as frames from '../frames';
import type * as types from '../types';
import * as bidi from './third_party/bidiProtocol';
import type { BidiSession } from './bidiConnection';
export class BidiNetworkManager {
private readonly _session: BidiSession;
private readonly _requests: Map<string, BidiRequest>;
private readonly _page: Page;
private readonly _eventListeners: RegisteredListener[];
private readonly _onNavigationResponseStarted: (params: bidi.Network.ResponseStartedParameters) => void;
private _userRequestInterceptionEnabled: boolean = false;
private _protocolRequestInterceptionEnabled: boolean = false;
private _credentials: types.Credentials | undefined;
private _intercepId: bidi.Network.Intercept | undefined;
constructor(bidiSession: BidiSession, page: Page, onNavigationResponseStarted: (params: bidi.Network.ResponseStartedParameters) => void) {
this._session = bidiSession;
this._requests = new Map();
this._page = page;
this._onNavigationResponseStarted = onNavigationResponseStarted;
this._eventListeners = [
eventsHelper.addEventListener(bidiSession, 'network.beforeRequestSent', this._onBeforeRequestSent.bind(this)),
eventsHelper.addEventListener(bidiSession, 'network.responseStarted', this._onResponseStarted.bind(this)),
eventsHelper.addEventListener(bidiSession, 'network.responseCompleted', this._onResponseCompleted.bind(this)),
eventsHelper.addEventListener(bidiSession, 'network.fetchError', this._onFetchError.bind(this)),
eventsHelper.addEventListener(bidiSession, 'network.authRequired', this._onAuthRequired.bind(this)),
];
}
dispose() {
eventsHelper.removeEventListeners(this._eventListeners);
}
private _onBeforeRequestSent(param: bidi.Network.BeforeRequestSentParameters) {
if (param.request.url.startsWith('data:'))
return;
const redirectedFrom = param.redirectCount ? (this._requests.get(param.request.request) || null) : null;
const frame = redirectedFrom ? redirectedFrom.request.frame() : (param.context ? this._page._frameManager.frame(param.context) : null);
if (!frame)
return;
if (redirectedFrom)
this._requests.delete(redirectedFrom._id);
let route;
if (param.intercepts) {
// We do not support intercepting redirects.
if (redirectedFrom) {
this._session.sendMayFail('network.continueRequest', {
request: param.request.request,
headers: redirectedFrom._originalRequestRoute?._alreadyContinuedHeaders,
});
} else {
route = new BidiRouteImpl(this._session, param.request.request);
}
}
const request = new BidiRequest(frame, redirectedFrom, param, route);
this._requests.set(request._id, request);
this._page._frameManager.requestStarted(request.request, route);
}
private _onResponseStarted(params: bidi.Network.ResponseStartedParameters) {
const request = this._requests.get(params.request.request);
if (!request)
return;
const getResponseBody = async () => {
throw new Error(`Response body is not available for requests in Bidi`);
};
const timings = params.request.timings;
const startTime = timings.requestTime;
function relativeToStart(time: number): number {
if (!time)
return -1;
return (time - startTime) / 1000;
}
const timing: network.ResourceTiming = {
startTime: startTime / 1000,
requestStart: relativeToStart(timings.requestStart),
responseStart: relativeToStart(timings.responseStart),
domainLookupStart: relativeToStart(timings.dnsStart),
domainLookupEnd: relativeToStart(timings.dnsEnd),
connectStart: relativeToStart(timings.connectStart),
secureConnectionStart: relativeToStart(timings.tlsStart),
connectEnd: relativeToStart(timings.connectEnd),
};
const response = new network.Response(request.request, params.response.status, params.response.statusText, fromBidiHeaders(params.response.headers), timing, getResponseBody, false);
response._serverAddrFinished();
response._securityDetailsFinished();
// "raw" headers are the same as "provisional" headers in Bidi.
response.setRawResponseHeaders(null);
response.setResponseHeadersSize(params.response.headersSize);
this._page._frameManager.requestReceivedResponse(response);
if (params.navigation)
this._onNavigationResponseStarted(params);
}
private _onResponseCompleted(params: bidi.Network.ResponseCompletedParameters) {
const request = this._requests.get(params.request.request);
if (!request)
return;
const response = request.request._existingResponse()!;
// TODO: body size is the encoded size
response.setTransferSize(params.response.bodySize);
response.setEncodedBodySize(params.response.bodySize);
// Keep redirected requests in the map for future reference as redirectedFrom.
const isRedirected = response.status() >= 300 && response.status() <= 399;
const responseEndTime = params.request.timings.responseEnd / 1000 - response.timing().startTime;
if (isRedirected) {
response._requestFinished(responseEndTime);
} else {
this._requests.delete(request._id);
response._requestFinished(responseEndTime);
}
response._setHttpVersion(params.response.protocol);
this._page._frameManager.reportRequestFinished(request.request, response);
}
private _onFetchError(params: bidi.Network.FetchErrorParameters) {
const request = this._requests.get(params.request.request);
if (!request)
return;
this._requests.delete(request._id);
const response = request.request._existingResponse();
if (response) {
response.setTransferSize(null);
response.setEncodedBodySize(null);
response._requestFinished(-1);
}
request.request._setFailureText(params.errorText);
// TODO: support canceled flag
this._page._frameManager.requestFailed(request.request, params.errorText === 'NS_BINDING_ABORTED');
}
private _onAuthRequired(params: bidi.Network.AuthRequiredParameters) {
const isBasic = params.response.authChallenges?.some(challenge => challenge.scheme.startsWith('Basic'));
const credentials = this._page._browserContext._options.httpCredentials;
if (isBasic && credentials) {
this._session.sendMayFail('network.continueWithAuth', {
request: params.request.request,
action: 'provideCredentials',
credentials: {
type: 'password',
username: credentials.username,
password: credentials.password,
}
});
} else {
this._session.sendMayFail('network.continueWithAuth', {
request: params.request.request,
action: 'default',
});
}
}
async setRequestInterception(value: boolean) {
this._userRequestInterceptionEnabled = value;
await this._updateProtocolRequestInterception();
}
async setCredentials(credentials: types.Credentials | undefined) {
this._credentials = credentials;
await this._updateProtocolRequestInterception();
}
async _updateProtocolRequestInterception(initial?: boolean) {
const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
if (enabled === this._protocolRequestInterceptionEnabled)
return;
this._protocolRequestInterceptionEnabled = enabled;
if (initial && !enabled)
return;
const cachePromise = this._session.send('network.setCacheBehavior', { cacheBehavior: enabled ? 'bypass' : 'default' });
let interceptPromise = Promise.resolve<any>(undefined);
if (enabled) {
interceptPromise = this._session.send('network.addIntercept', {
phases: [bidi.Network.InterceptPhase.AuthRequired, bidi.Network.InterceptPhase.BeforeRequestSent],
urlPatterns: [{ type: 'pattern' }],
// urlPatterns: [{ type: 'string', pattern: '*' }],
}).then(r => {
this._intercepId = r.intercept;
});
} else if (this._intercepId) {
interceptPromise = this._session.send('network.removeIntercept', { intercept: this._intercepId });
this._intercepId = undefined;
}
await Promise.all([cachePromise, interceptPromise]);
}
}
class BidiRequest {
readonly request: network.Request;
readonly _id: string;
private _redirectedTo: BidiRequest | undefined;
// Only first request in the chain can be intercepted, so this will
// store the first and only Route in the chain (if any).
_originalRequestRoute: BidiRouteImpl | undefined;
constructor(frame: frames.Frame, redirectedFrom: BidiRequest | null, payload: bidi.Network.BeforeRequestSentParameters, route: BidiRouteImpl | undefined) {
this._id = payload.request.request;
if (redirectedFrom)
redirectedFrom._redirectedTo = this;
// TODO: missing in the spec?
const postDataBuffer = null;
this.request = new network.Request(frame._page._browserContext, frame, null, redirectedFrom ? redirectedFrom.request : null, payload.navigation ?? undefined,
payload.request.url, 'other', payload.request.method, postDataBuffer, fromBidiHeaders(payload.request.headers));
// "raw" headers are the same as "provisional" headers in Bidi.
this.request.setRawRequestHeaders(null);
this.request._setBodySize(payload.request.bodySize || 0);
this._originalRequestRoute = route ?? redirectedFrom?._originalRequestRoute;
route?._setRequest(this.request);
}
_finalRequest(): BidiRequest {
let request: BidiRequest = this;
while (request._redirectedTo)
request = request._redirectedTo;
return request;
}
}
class BidiRouteImpl implements network.RouteDelegate {
private _requestId: bidi.Network.Request;
private _session: BidiSession;
private _request!: network.Request;
_alreadyContinuedHeaders: bidi.Network.Header[] | undefined;
constructor(session: BidiSession, requestId: bidi.Network.Request) {
this._session = session;
this._requestId = requestId;
}
_setRequest(request: network.Request) {
this._request = request;
}
async continue(overrides: types.NormalizedContinueOverrides) {
// Firefox does not update content-length header.
let headers = overrides.headers || this._request.headers();
if (overrides.postData && headers) {
headers = headers.map(header => {
if (header.name.toLowerCase() === 'content-length')
return { name: header.name, value: overrides.postData!.byteLength.toString() };
return header;
});
}
this._alreadyContinuedHeaders = toBidiHeaders(headers);
await this._session.sendMayFail('network.continueRequest', {
request: this._requestId,
url: overrides.url,
method: overrides.method,
// TODO: cookies!
headers: this._alreadyContinuedHeaders,
body: overrides.postData ? { type: 'base64', value: Buffer.from(overrides.postData).toString('base64') } : undefined,
});
}
async fulfill(response: types.NormalizedFulfillResponse) {
const base64body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64');
await this._session.sendMayFail('network.provideResponse', {
request: this._requestId,
statusCode: response.status,
reasonPhrase: network.statusText(response.status),
headers: toBidiHeaders(response.headers),
body: { type: 'base64', value: base64body },
});
}
async abort(errorCode: string) {
await this._session.sendMayFail('network.failRequest', {
request: this._requestId
});
}
}
function fromBidiHeaders(bidiHeaders: bidi.Network.Header[]): types.HeadersArray {
const result: types.HeadersArray = [];
for (const { name, value } of bidiHeaders)
result.push({ name, value: bidiBytesValueToString(value) });
return result;
}
function toBidiHeaders(headers: types.HeadersArray): bidi.Network.Header[] {
return headers.map(({ name, value }) => ({ name, value: { type: 'string', value } }));
}
export function bidiBytesValueToString(value: bidi.Network.BytesValue): string {
if (value.type === 'string')
return value.value;
if (value.type === 'base64')
return Buffer.from(value.type, 'base64').toString('binary');
return 'unknown value type: ' + (value as any).type;
}

View file

@ -0,0 +1,100 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as bidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/BidiMapper';
import * as bidiCdpConnection from 'chromium-bidi/lib/cjs/cdp/CdpConnection';
import type * as bidiTransport from 'chromium-bidi/lib/cjs/utils/transport';
import type { ChromiumBidi } from 'chromium-bidi/lib/cjs/protocol/protocol';
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { debugLogger } from '../../utils/debugLogger';
const bidiServerLogger = (prefix: string, ...args: unknown[]): void => {
debugLogger.log(prefix as any, args);
};
export async function connectBidiOverCdp(cdp: ConnectionTransport): Promise<ConnectionTransport> {
let server: bidiMapper.BidiServer | undefined = undefined;
const bidiTransport = new BidiTransportImpl();
const bidiConnection = new BidiConnection(bidiTransport, () => server?.close());
const cdpTransportImpl = new CdpTransportImpl(cdp);
const cdpConnection = new bidiCdpConnection.MapperCdpConnection(cdpTransportImpl, bidiServerLogger);
// Make sure onclose event is propagated.
cdp.onclose = () => bidiConnection.onclose?.();
server = await bidiMapper.BidiServer.createAndStart(
bidiTransport,
cdpConnection,
await cdpConnection.createBrowserSession(),
/* selfTargetId= */ '',
undefined,
bidiServerLogger);
return bidiConnection;
}
class BidiTransportImpl implements bidiMapper.BidiTransport {
_handler?: (message: ChromiumBidi.Command) => Promise<void> | void;
_bidiConnection!: BidiConnection;
setOnMessage(handler: (message: ChromiumBidi.Command) => Promise<void> | void) {
this._handler = handler;
}
sendMessage(message: ChromiumBidi.Message): Promise<void> | void {
return this._bidiConnection.onmessage?.(message as any);
}
close() {
this._bidiConnection.onclose?.();
}
}
class BidiConnection implements ConnectionTransport {
private _bidiTransport: BidiTransportImpl;
private _closeCallback: () => void;
constructor(bidiTransport: BidiTransportImpl, closeCallback: () => void) {
this._bidiTransport = bidiTransport;
this._bidiTransport._bidiConnection = this;
this._closeCallback = closeCallback;
}
send(s: ProtocolRequest): void {
this._bidiTransport._handler?.(s as any);
}
close(): void {
this._closeCallback();
}
onmessage?: ((message: ProtocolResponse) => void) | undefined;
onclose?: ((reason?: string) => void) | undefined;
}
class CdpTransportImpl implements bidiTransport.Transport {
private _connection: ConnectionTransport;
private _handler?: (message: string) => Promise<void> | void;
_bidiConnection!: BidiConnection;
constructor(connection: ConnectionTransport) {
this._connection = connection;
this._connection.onmessage = message => {
this._handler?.(JSON.stringify(message));
};
}
setOnMessage(handler: (message: string) => Promise<void> | void) {
this._handler = handler;
}
sendMessage(message: string): Promise<void> | void {
return this._connection.send(JSON.parse(message));
}
close(): void {
this._connection.close();
}
}

View file

@ -0,0 +1,525 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { RegisteredListener } from '../../utils/eventsHelper';
import { eventsHelper } from '../../utils/eventsHelper';
import { assert } from '../../utils';
import type * as accessibility from '../accessibility';
import * as dom from '../dom';
import * as dialog from '../dialog';
import type * as frames from '../frames';
import { type InitScript, Page, type PageDelegate } from '../page';
import type { Progress } from '../progress';
import type * as types from '../types';
import type { BidiBrowserContext } from './bidiBrowser';
import type { BidiSession } from './bidiConnection';
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './bidiInput';
import * as bidi from './third_party/bidiProtocol';
import { BidiExecutionContext } from './bidiExecutionContext';
import { BidiNetworkManager } from './bidiNetworkManager';
import { BrowserContext } from '../browserContext';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
export class BidiPage implements PageDelegate {
readonly rawMouse: RawMouseImpl;
readonly rawKeyboard: RawKeyboardImpl;
readonly rawTouchscreen: RawTouchscreenImpl;
readonly _page: Page;
private readonly _pagePromise: Promise<Page | Error>;
readonly _session: BidiSession;
readonly _opener: BidiPage | null;
private readonly _realmToContext: Map<string, dom.FrameExecutionContext>;
private _sessionListeners: RegisteredListener[] = [];
readonly _browserContext: BidiBrowserContext;
readonly _networkManager: BidiNetworkManager;
_initializedPage: Page | null = null;
private _initScriptIds: string[] = [];
constructor(browserContext: BidiBrowserContext, bidiSession: BidiSession, opener: BidiPage | null) {
this._session = bidiSession;
this._opener = opener;
this.rawKeyboard = new RawKeyboardImpl(bidiSession);
this.rawMouse = new RawMouseImpl(bidiSession);
this.rawTouchscreen = new RawTouchscreenImpl(bidiSession);
this._realmToContext = new Map();
this._page = new Page(this, browserContext);
this._browserContext = browserContext;
this._networkManager = new BidiNetworkManager(this._session, this._page, this._onNavigationResponseStarted.bind(this));
this._page.on(Page.Events.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false));
this._sessionListeners = [
eventsHelper.addEventListener(bidiSession, 'script.realmCreated', this._onRealmCreated.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.contextDestroyed', this._onBrowsingContextDestroyed.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationStarted', this._onNavigationStarted.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationAborted', this._onNavigationAborted.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationFailed', this._onNavigationFailed.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.fragmentNavigated', this._onFragmentNavigated.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.domContentLoaded', this._onDomContentLoaded.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.load', this._onLoad.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.userPromptOpened', this._onUserPromptOpened.bind(this)),
eventsHelper.addEventListener(bidiSession, 'log.entryAdded', this._onLogEntryAdded.bind(this)),
];
// Initialize main frame.
this._pagePromise = this._initialize().finally(async () => {
await this._page.initOpener(this._opener);
}).then(() => {
this._initializedPage = this._page;
this._page.reportAsNew();
return this._page;
}).catch(e => {
this._page.reportAsNew(e);
return e;
});
}
private async _initialize() {
// Initialize main frame.
this._onFrameAttached(this._session.sessionId, null);
await Promise.all([
this.updateHttpCredentials(),
this.updateRequestInterception(),
this._updateViewport(),
this._addAllInitScripts(),
]);
}
private async _addAllInitScripts() {
return Promise.all(this._page.allInitScripts().map(initScript => this.addInitScript(initScript)));
}
potentiallyUninitializedPage(): Page {
return this._page;
}
didClose() {
this._session.dispose();
eventsHelper.removeEventListeners(this._sessionListeners);
this._page._didClose();
}
async pageOrError(): Promise<Page | Error> {
// TODO: Wait for first execution context to be created and maybe about:blank navigated.
return this._pagePromise;
}
private _onFrameAttached(frameId: string, parentFrameId: string | null): frames.Frame {
return this._page._frameManager.frameAttached(frameId, parentFrameId);
}
private _removeContextsForFrame(frame: frames.Frame, notifyFrame: boolean) {
for (const [contextId, context] of this._realmToContext) {
if (context.frame === frame) {
this._realmToContext.delete(contextId);
if (notifyFrame)
frame._contextDestroyed(context);
}
}
}
private _onRealmCreated(realmInfo: bidi.Script.RealmInfo) {
if (this._realmToContext.has(realmInfo.realm))
return;
if (realmInfo.type !== 'window')
return;
const frame = this._page._frameManager.frame(realmInfo.context);
if (!frame)
return;
const delegate = new BidiExecutionContext(this._session, realmInfo);
let worldName: types.World;
if (!realmInfo.sandbox) {
worldName = 'main';
// Force creating utility world every time the main world is created (e.g. due to navigation).
this._touchUtilityWorld(realmInfo.context);
} else if (realmInfo.sandbox === UTILITY_WORLD_NAME) {
worldName = 'utility';
} else {
return;
}
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
(context as any)[contextDelegateSymbol] = delegate;
frame._contextCreated(worldName, context);
this._realmToContext.set(realmInfo.realm, context);
}
private async _touchUtilityWorld(context: bidi.BrowsingContext.BrowsingContext) {
await this._session.sendMayFail('script.evaluate', {
expression: '1 + 1',
target: {
context,
sandbox: UTILITY_WORLD_NAME,
},
serializationOptions: {
maxObjectDepth: 10,
maxDomDepth: 10,
},
awaitPromise: true,
userActivation: true,
});
}
_onRealmDestroyed(params: bidi.Script.RealmDestroyedParameters): boolean {
const context = this._realmToContext.get(params.realm);
if (!context)
return false;
this._realmToContext.delete(params.realm);
context.frame._contextDestroyed(context);
return true;
}
// TODO: route the message directly to the browser
private _onBrowsingContextDestroyed(params: bidi.BrowsingContext.Info) {
this._browserContext._browser._onBrowsingContextDestroyed(params);
}
private _onNavigationStarted(params: bidi.BrowsingContext.NavigationInfo) {
const frameId = params.context;
this._page._frameManager.frameRequestedNavigation(frameId, params.navigation!);
const url = params.url.toLowerCase();
if (url.startsWith('file:') || url.startsWith('data:') || url === 'about:blank') {
// Navigation to file urls doesn't emit network events, so we fire 'commit' event right when navigation is started.
// Doing it in domcontentload would be too late as we'd clear frame tree.
const frame = this._page._frameManager.frame(frameId)!;
if (frame)
this._page._frameManager.frameCommittedNewDocumentNavigation(frameId, params.url, '', params.navigation!, /* initial */ false);
}
}
// TODO: there is no separate event for committed navigation, so we approximate it with responseStarted.
private _onNavigationResponseStarted(params: bidi.Network.ResponseStartedParameters) {
const frameId = params.context!;
const frame = this._page._frameManager.frame(frameId);
assert(frame);
this._page._frameManager.frameCommittedNewDocumentNavigation(frameId, params.response.url, '', params.navigation!, /* initial */ false);
// if (!initial)
// this._firstNonInitialNavigationCommittedFulfill();
}
private _onDomContentLoaded(params: bidi.BrowsingContext.NavigationInfo) {
const frameId = params.context;
this._page._frameManager.frameLifecycleEvent(frameId, 'domcontentloaded');
}
private _onLoad(params: bidi.BrowsingContext.NavigationInfo) {
this._page._frameManager.frameLifecycleEvent(params.context, 'load');
}
private _onNavigationAborted(params: bidi.BrowsingContext.NavigationInfo) {
this._page._frameManager.frameAbortedNavigation(params.context, 'Navigation aborted', params.navigation || undefined);
}
private _onNavigationFailed(params: bidi.BrowsingContext.NavigationInfo) {
this._page._frameManager.frameAbortedNavigation(params.context, 'Navigation failed', params.navigation || undefined);
}
private _onFragmentNavigated(params: bidi.BrowsingContext.NavigationInfo) {
this._page._frameManager.frameCommittedSameDocumentNavigation(params.context, params.url);
}
private _onUserPromptOpened(event: bidi.BrowsingContext.UserPromptOpenedParameters) {
this._page.emitOnContext(BrowserContext.Events.Dialog, new dialog.Dialog(
this._page,
event.type as dialog.DialogType,
event.message,
async (accept: boolean, userText?: string) => {
await this._session.send('browsingContext.handleUserPrompt', { context: event.context, accept, userText });
},
event.defaultValue));
}
private _onLogEntryAdded(params: bidi.Log.Entry) {
if (params.type !== 'console')
return;
const entry: bidi.Log.ConsoleLogEntry = params as bidi.Log.ConsoleLogEntry;
const context = this._realmToContext.get(params.source.realm);
if (!context)
return;
const callFrame = params.stackTrace?.callFrames[0];
const location = callFrame ?? { url: '', lineNumber: 1, columnNumber: 1 };
this._page._addConsoleMessage(entry.method, entry.args.map(arg => context.createHandle({ objectId: (arg as any).handle, ...arg })), location, params.text || undefined);
}
async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
const { navigation } = await this._session.send('browsingContext.navigate', {
context: frame._id,
url,
});
return { newDocumentId: navigation || undefined };
}
async updateExtraHTTPHeaders(): Promise<void> {
}
async updateEmulateMedia(): Promise<void> {
}
async updateEmulatedViewportSize(): Promise<void> {
await this._updateViewport();
}
async updateUserAgent(): Promise<void> {
}
async bringToFront(): Promise<void> {
}
private async _updateViewport(): Promise<void> {
const options = this._browserContext._options;
const deviceSize = this._page.emulatedSize();
if (deviceSize === null)
return;
const viewportSize = deviceSize.viewport;
await this._session.send('browsingContext.setViewport', {
context: this._session.sessionId,
viewport: {
width: viewportSize.width,
height: viewportSize.height,
},
devicePixelRatio: options.deviceScaleFactor || 1
});
}
async updateRequestInterception(): Promise<void> {
await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
}
async updateOffline() {
}
async updateHttpCredentials() {
await this._networkManager.setCredentials(this._browserContext._options.httpCredentials);
}
async updateFileChooserInterception() {
}
async reload(): Promise<void> {
await this._session.send('browsingContext.reload', {
context: this._session.sessionId,
// ignoreCache: true,
wait: bidi.BrowsingContext.ReadinessState.Interactive,
});
}
goBack(): Promise<boolean> {
throw new Error('Method not implemented.');
}
goForward(): Promise<boolean> {
throw new Error('Method not implemented.');
}
async addInitScript(initScript: InitScript): Promise<void> {
const { script } = await this._session.send('script.addPreloadScript', {
// TODO: remove function call from the source.
functionDeclaration: `() => { return ${initScript.source} }`,
// TODO: push to iframes?
contexts: [this._session.sessionId],
});
if (!initScript.internal)
this._initScriptIds.push(script);
}
async removeNonInternalInitScripts() {
const promises = this._initScriptIds.map(script => this._session.send('script.removePreloadScript', { script }));
this._initScriptIds = [];
await Promise.all(promises);
}
async closePage(runBeforeUnload: boolean): Promise<void> {
await this._session.send('browsingContext.close', {
context: this._session.sessionId,
promptUnload: runBeforeUnload,
});
}
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
}
async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
throw new Error('Method not implemented.');
}
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
const executionContext = toBidiExecutionContext(handle._context);
const contentWindow = await executionContext.rawCallFunction('e => e.contentWindow', { handle: handle._objectId });
if (contentWindow.type === 'window') {
const frameId = contentWindow.value.context;
const result = this._page._frameManager.frame(frameId);
return result;
}
return null;
}
async getOwnerFrame(handle: dom.ElementHandle): Promise<string | null> {
throw new Error('Method not implemented.');
}
isElementHandle(remoteObject: bidi.Script.RemoteValue): boolean {
return remoteObject.type === 'node';
}
async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
const box = await handle.evaluate(element => {
if (!(element instanceof Element))
return null;
const rect = element.getBoundingClientRect();
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
});
if (!box)
return null;
const position = await this._framePosition(handle._frame);
if (!position)
return null;
box.x += position.x;
box.y += position.y;
return box;
}
// TODO: move to Frame.
private async _framePosition(frame: frames.Frame): Promise<types.Point | null> {
if (frame === this._page.mainFrame())
return { x: 0, y: 0 };
const element = await frame.frameElement();
const box = await element.boundingBox();
if (!box)
return null;
const style = await element.evaluateInUtility(([injected, iframe]) => injected.describeIFrameStyle(iframe as Element), {}).catch(e => 'error:notconnected' as const);
if (style === 'error:notconnected' || style === 'transformed')
return null;
// Content box is offset by border and padding widths.
box.x += style.left;
box.y += style.top;
return box;
}
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle<Element>, rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'> {
return await handle.evaluateInUtility(([injected, node]) => {
node.scrollIntoView({
block: 'center',
inline: 'center',
behavior: 'instant',
});
}, null).then(() => 'done' as const).catch(e => {
if (e instanceof Error && e.message.includes('Node is detached from document'))
return 'error:notconnected';
if (e instanceof Error && e.message.includes('Node does not have a layout object'))
return 'error:notvisible';
throw e;
});
}
async setScreencastOptions(options: { width: number, height: number, quality: number } | null): Promise<void> {
}
rafCountForStablePosition(): number {
return 1;
}
async getContentQuads(handle: dom.ElementHandle<Element>): Promise<types.Quad[] | null | 'error:notconnected'> {
const quads = await handle.evaluateInUtility(([injected, node]) => {
if (!node.isConnected)
return 'error:notconnected';
const rects = node.getClientRects();
if (!rects)
return null;
return [...rects].map(rect => [
{ x: rect.left, y: rect.top },
{ x: rect.right, y: rect.top },
{ x: rect.right, y: rect.bottom },
{ x: rect.left, y: rect.bottom },
]);
}, null);
if (!quads || quads === 'error:notconnected')
return quads;
// TODO: consider transforming quads to support clicks in iframes.
const position = await this._framePosition(handle._frame);
if (!position)
return null;
quads.forEach(quad => quad.forEach(point => {
point.x += position.x;
point.y += position.y;
}));
return quads as types.Quad[];
}
async setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void> {
throw new Error('Method not implemented.');
}
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, paths: string[]): Promise<void> {
throw new Error('Method not implemented.');
}
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
const fromContext = toBidiExecutionContext(handle._context);
const shared = await fromContext.rawCallFunction('x => x', { handle: handle._objectId });
// TODO: store sharedId in the handle.
if (!('sharedId' in shared))
throw new Error('Element is not a node');
const sharedId = shared.sharedId!;
const executionContext = toBidiExecutionContext(to);
const result = await executionContext.rawCallFunction('x => x', { sharedId });
if ('handle' in result)
return to.createHandle({ objectId: result.handle!, ...result }) as dom.ElementHandle<T>;
throw new Error('Failed to adopt element handle.');
}
async getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> {
throw new Error('Method not implemented.');
}
async inputActionEpilogue(): Promise<void> {
}
async resetForReuse(): Promise<void> {
}
async getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle> {
const parent = frame.parentFrame();
if (!parent)
throw new Error('Frame has been detached.');
const parentContext = await parent._mainContext();
const list = await parentContext.evaluateHandle(() => { return [...document.querySelectorAll('iframe,frame')]; });
const length = await list.evaluate(list => list.length);
let foundElement = null;
for (let i = 0; i < length; i++) {
const element = await list.evaluateHandle((list, i) => list[i], i);
const candidate = await element.contentFrame();
if (frame === candidate) {
foundElement = element;
break;
} else {
element.dispose();
}
}
list.dispose();
if (!foundElement)
throw new Error('Frame has been detached.');
return foundElement;
}
shouldToggleStyleSheetToSyncAnimations(): boolean {
return true;
}
}
function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext {
return (executionContext as any)[contextDelegateSymbol] as BidiExecutionContext;
}
const contextDelegateSymbol = Symbol('delegate');

View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,176 @@
/**
* @license
* Copyright 2024 Google Inc.
* Modifications copyright (c) Microsoft Corporation.
* SPDX-License-Identifier: Apache-2.0
*/
import * as Bidi from './bidiProtocol';
export interface Commands {
'script.evaluate': {
params: Bidi.Script.EvaluateParameters;
returnType: Bidi.Script.EvaluateResult;
};
'script.callFunction': {
params: Bidi.Script.CallFunctionParameters;
returnType: Bidi.Script.EvaluateResult;
};
'script.disown': {
params: Bidi.Script.DisownParameters;
returnType: Bidi.EmptyResult;
};
'script.addPreloadScript': {
params: Bidi.Script.AddPreloadScriptParameters;
returnType: Bidi.Script.AddPreloadScriptResult;
};
'script.removePreloadScript': {
params: Bidi.Script.RemovePreloadScriptParameters;
returnType: Bidi.EmptyResult;
};
'browser.close': {
params: Bidi.EmptyParams;
returnType: Bidi.EmptyResult;
};
'browser.createUserContext': {
params: Bidi.EmptyParams;
returnType: Bidi.Browser.CreateUserContextResult;
};
'browser.getUserContexts': {
params: Bidi.EmptyParams;
returnType: Bidi.Browser.GetUserContextsResult;
};
'browser.removeUserContext': {
params: {
userContext: Bidi.Browser.UserContext;
};
returnType: Bidi.Browser.RemoveUserContext;
};
'browsingContext.activate': {
params: Bidi.BrowsingContext.ActivateParameters;
returnType: Bidi.EmptyResult;
};
'browsingContext.create': {
params: Bidi.BrowsingContext.CreateParameters;
returnType: Bidi.BrowsingContext.CreateResult;
};
'browsingContext.close': {
params: Bidi.BrowsingContext.CloseParameters;
returnType: Bidi.EmptyResult;
};
'browsingContext.getTree': {
params: Bidi.BrowsingContext.GetTreeParameters;
returnType: Bidi.BrowsingContext.GetTreeResult;
};
'browsingContext.locateNodes': {
params: Bidi.BrowsingContext.LocateNodesParameters;
returnType: Bidi.BrowsingContext.LocateNodesResult;
};
'browsingContext.navigate': {
params: Bidi.BrowsingContext.NavigateParameters;
returnType: Bidi.BrowsingContext.NavigateResult;
};
'browsingContext.reload': {
params: Bidi.BrowsingContext.ReloadParameters;
returnType: Bidi.BrowsingContext.NavigateResult;
};
'browsingContext.print': {
params: Bidi.BrowsingContext.PrintParameters;
returnType: Bidi.BrowsingContext.PrintResult;
};
'browsingContext.captureScreenshot': {
params: Bidi.BrowsingContext.CaptureScreenshotParameters;
returnType: Bidi.BrowsingContext.CaptureScreenshotResult;
};
'browsingContext.handleUserPrompt': {
params: Bidi.BrowsingContext.HandleUserPromptParameters;
returnType: Bidi.EmptyResult;
};
'browsingContext.setViewport': {
params: Bidi.BrowsingContext.SetViewportParameters;
returnType: Bidi.EmptyResult;
};
'browsingContext.traverseHistory': {
params: Bidi.BrowsingContext.TraverseHistoryParameters;
returnType: Bidi.EmptyResult;
};
'input.performActions': {
params: Bidi.Input.PerformActionsParameters;
returnType: Bidi.EmptyResult;
};
'input.releaseActions': {
params: Bidi.Input.ReleaseActionsParameters;
returnType: Bidi.EmptyResult;
};
'input.setFiles': {
params: Bidi.Input.SetFilesParameters;
returnType: Bidi.EmptyResult;
};
'session.end': {
params: Bidi.EmptyParams;
returnType: Bidi.EmptyResult;
};
'session.new': {
params: Bidi.Session.NewParameters;
returnType: Bidi.Session.NewResult;
};
'session.status': {
params: object;
returnType: Bidi.Session.StatusResult;
};
'session.subscribe': {
params: Bidi.Session.SubscriptionRequest;
returnType: Bidi.EmptyResult;
};
'session.unsubscribe': {
params: Bidi.Session.SubscriptionRequest;
returnType: Bidi.EmptyResult;
};
'storage.deleteCookies': {
params: Bidi.Storage.DeleteCookiesParameters;
returnType: Bidi.Storage.DeleteCookiesResult;
};
'storage.getCookies': {
params: Bidi.Storage.GetCookiesParameters;
returnType: Bidi.Storage.GetCookiesResult;
};
'network.setCacheBehavior': {
params: Bidi.Network.SetCacheBehaviorParameters;
returnType: Bidi.EmptyResult;
};
'storage.setCookie': {
params: Bidi.Storage.SetCookieParameters;
returnType: Bidi.Storage.SetCookieParameters;
};
'network.addIntercept': {
params: Bidi.Network.AddInterceptParameters;
returnType: Bidi.Network.AddInterceptResult;
};
'network.removeIntercept': {
params: Bidi.Network.RemoveInterceptParameters;
returnType: Bidi.EmptyResult;
};
'network.continueRequest': {
params: Bidi.Network.ContinueRequestParameters;
returnType: Bidi.EmptyResult;
};
'network.continueWithAuth': {
params: Bidi.Network.ContinueWithAuthParameters;
returnType: Bidi.EmptyResult;
};
'network.failRequest': {
params: Bidi.Network.FailRequestParameters;
returnType: Bidi.EmptyResult;
};
'network.provideResponse': {
params: Bidi.Network.ProvideResponseParameters;
returnType: Bidi.EmptyResult;
};
}

View file

@ -0,0 +1,91 @@
/**
* @license
* Copyright 2024 Google Inc.
* Modifications copyright (c) Microsoft Corporation.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from './bidiProtocol';
/* eslint-disable object-curly-spacing */
/**
* @internal
*/
export class BidiDeserializer {
static deserialize(result: Bidi.Script.RemoteValue): any {
if (!result)
return undefined;
switch (result.type) {
case 'array':
return result.value?.map(value => {
return BidiDeserializer.deserialize(value);
});
case 'set':
return result.value?.reduce((acc: Set<unknown>, value) => {
return acc.add(BidiDeserializer.deserialize(value));
}, new Set());
case 'object':
return result.value?.reduce((acc: Record<any, unknown>, tuple) => {
const {key, value} = BidiDeserializer._deserializeTuple(tuple);
acc[key as any] = value;
return acc;
}, {});
case 'map':
return result.value?.reduce((acc: Map<unknown, unknown>, tuple) => {
const {key, value} = BidiDeserializer._deserializeTuple(tuple);
return acc.set(key, value);
}, new Map());
case 'promise':
return {};
case 'regexp':
return new RegExp(result.value.pattern, result.value.flags);
case 'date':
return new Date(result.value);
case 'undefined':
return undefined;
case 'null':
return null;
case 'number':
return BidiDeserializer._deserializeNumber(result.value);
case 'bigint':
return BigInt(result.value);
case 'boolean':
return Boolean(result.value);
case 'string':
return result.value;
}
throw new Error(`Deserialization of type ${result.type} not supported.`);
}
static _deserializeNumber(value: Bidi.Script.SpecialNumber | number): number {
switch (value) {
case '-0':
return -0;
case 'NaN':
return NaN;
case 'Infinity':
return Infinity;
case '-Infinity':
return -Infinity;
default:
return value;
}
}
static _deserializeTuple([serializedKey, serializedValue]: [
Bidi.Script.RemoteValue | string,
Bidi.Script.RemoteValue,
]): {key: unknown; value: unknown} {
const key =
typeof serializedKey === 'string'
? serializedKey
: BidiDeserializer.deserialize(serializedKey);
const value = BidiDeserializer.deserialize(serializedValue);
return {key, value};
}
}

View file

@ -0,0 +1,231 @@
/**
* @license
* Copyright 2024 Google Inc.
* Modifications copyright (c) Microsoft Corporation.
* SPDX-License-Identifier: Apache-2.0
*/
/* eslint-disable curly */
export const getBidiKeyValue = (key: string) => {
switch (key) {
case '\r':
case '\n':
key = 'Enter';
break;
}
// Measures the number of code points rather than UTF-16 code units.
if ([...key].length === 1) {
return key;
}
switch (key) {
case 'Cancel':
return '\uE001';
case 'Help':
return '\uE002';
case 'Backspace':
return '\uE003';
case 'Tab':
return '\uE004';
case 'Clear':
return '\uE005';
case 'Enter':
return '\uE007';
case 'Shift':
case 'ShiftLeft':
return '\uE008';
case 'Control':
case 'ControlLeft':
return '\uE009';
case 'Alt':
case 'AltLeft':
return '\uE00A';
case 'Pause':
return '\uE00B';
case 'Escape':
return '\uE00C';
case 'PageUp':
return '\uE00E';
case 'PageDown':
return '\uE00F';
case 'End':
return '\uE010';
case 'Home':
return '\uE011';
case 'ArrowLeft':
return '\uE012';
case 'ArrowUp':
return '\uE013';
case 'ArrowRight':
return '\uE014';
case 'ArrowDown':
return '\uE015';
case 'Insert':
return '\uE016';
case 'Delete':
return '\uE017';
case 'NumpadEqual':
return '\uE019';
case 'Numpad0':
return '\uE01A';
case 'Numpad1':
return '\uE01B';
case 'Numpad2':
return '\uE01C';
case 'Numpad3':
return '\uE01D';
case 'Numpad4':
return '\uE01E';
case 'Numpad5':
return '\uE01F';
case 'Numpad6':
return '\uE020';
case 'Numpad7':
return '\uE021';
case 'Numpad8':
return '\uE022';
case 'Numpad9':
return '\uE023';
case 'NumpadMultiply':
return '\uE024';
case 'NumpadAdd':
return '\uE025';
case 'NumpadSubtract':
return '\uE027';
case 'NumpadDecimal':
return '\uE028';
case 'NumpadDivide':
return '\uE029';
case 'F1':
return '\uE031';
case 'F2':
return '\uE032';
case 'F3':
return '\uE033';
case 'F4':
return '\uE034';
case 'F5':
return '\uE035';
case 'F6':
return '\uE036';
case 'F7':
return '\uE037';
case 'F8':
return '\uE038';
case 'F9':
return '\uE039';
case 'F10':
return '\uE03A';
case 'F11':
return '\uE03B';
case 'F12':
return '\uE03C';
case 'Meta':
case 'MetaLeft':
return '\uE03D';
case 'ShiftRight':
return '\uE050';
case 'ControlRight':
return '\uE051';
case 'AltRight':
return '\uE052';
case 'MetaRight':
return '\uE053';
case 'Digit0':
return '0';
case 'Digit1':
return '1';
case 'Digit2':
return '2';
case 'Digit3':
return '3';
case 'Digit4':
return '4';
case 'Digit5':
return '5';
case 'Digit6':
return '6';
case 'Digit7':
return '7';
case 'Digit8':
return '8';
case 'Digit9':
return '9';
case 'KeyA':
return 'a';
case 'KeyB':
return 'b';
case 'KeyC':
return 'c';
case 'KeyD':
return 'd';
case 'KeyE':
return 'e';
case 'KeyF':
return 'f';
case 'KeyG':
return 'g';
case 'KeyH':
return 'h';
case 'KeyI':
return 'i';
case 'KeyJ':
return 'j';
case 'KeyK':
return 'k';
case 'KeyL':
return 'l';
case 'KeyM':
return 'm';
case 'KeyN':
return 'n';
case 'KeyO':
return 'o';
case 'KeyP':
return 'p';
case 'KeyQ':
return 'q';
case 'KeyR':
return 'r';
case 'KeyS':
return 's';
case 'KeyT':
return 't';
case 'KeyU':
return 'u';
case 'KeyV':
return 'v';
case 'KeyW':
return 'w';
case 'KeyX':
return 'x';
case 'KeyY':
return 'y';
case 'KeyZ':
return 'z';
case 'Semicolon':
return ';';
case 'Equal':
return '=';
case 'Comma':
return ',';
case 'Minus':
return '-';
case 'Period':
return '.';
case 'Slash':
return '/';
case 'Backquote':
return '`';
case 'BracketLeft':
return '[';
case 'Backslash':
return '\\';
case 'BracketRight':
return ']';
case 'Quote':
return '"';
default:
throw new Error(`Unknown key: "${key}"`);
}
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,148 @@
/**
* @license
* Copyright 2024 Google Inc.
* Modifications copyright (c) Microsoft Corporation.
* SPDX-License-Identifier: Apache-2.0
*/
import type * as Bidi from './bidiProtocol';
/* eslint-disable curly, indent */
/**
* @internal
*/
class UnserializableError extends Error {}
/**
* @internal
*/
export class BidiSerializer {
static serialize(arg: unknown): Bidi.Script.LocalValue {
switch (typeof arg) {
case 'symbol':
case 'function':
throw new UnserializableError(`Unable to serializable ${typeof arg}`);
case 'object':
return BidiSerializer._serializeObject(arg);
case 'undefined':
return {
type: 'undefined',
};
case 'number':
return BidiSerializer._serializeNumber(arg);
case 'bigint':
return {
type: 'bigint',
value: arg.toString(),
};
case 'string':
return {
type: 'string',
value: arg,
};
case 'boolean':
return {
type: 'boolean',
value: arg,
};
}
}
static _serializeNumber(arg: number): Bidi.Script.LocalValue {
let value: Bidi.Script.SpecialNumber | number;
if (Object.is(arg, -0)) {
value = '-0';
} else if (Object.is(arg, Infinity)) {
value = 'Infinity';
} else if (Object.is(arg, -Infinity)) {
value = '-Infinity';
} else if (Object.is(arg, NaN)) {
value = 'NaN';
} else {
value = arg;
}
return {
type: 'number',
value,
};
}
static _serializeObject(arg: object | null): Bidi.Script.LocalValue {
if (arg === null) {
return {
type: 'null',
};
} else if (Array.isArray(arg)) {
const parsedArray = arg.map(subArg => {
return BidiSerializer.serialize(subArg);
});
return {
type: 'array',
value: parsedArray,
};
} else if (isPlainObject(arg)) {
try {
JSON.stringify(arg);
} catch (error) {
if (
error instanceof TypeError &&
error.message.startsWith('Converting circular structure to JSON')
) {
error.message += ' Recursive objects are not allowed.';
}
throw error;
}
const parsedObject: Bidi.Script.MappingLocalValue = [];
for (const key in arg) {
parsedObject.push([BidiSerializer.serialize(key), BidiSerializer.serialize(arg[key])]);
}
return {
type: 'object',
value: parsedObject,
};
} else if (isRegExp(arg)) {
return {
type: 'regexp',
value: {
pattern: arg.source,
flags: arg.flags,
},
};
} else if (isDate(arg)) {
return {
type: 'date',
value: arg.toISOString(),
};
}
throw new UnserializableError(
'Custom object serialization not possible. Use plain objects instead.'
);
}
}
/**
* @internal
*/
export const isPlainObject = (obj: unknown): obj is Record<any, unknown> => {
return typeof obj === 'object' && obj?.constructor === Object;
};
/**
* @internal
*/
export const isRegExp = (obj: unknown): obj is RegExp => {
return typeof obj === 'object' && obj?.constructor === RegExp;
};
/**
* @internal
*/
export const isDate = (obj: unknown): obj is Date => {
return typeof obj === 'object' && obj?.constructor === Date;
};

View file

@ -15,7 +15,6 @@
* limitations under the License. * limitations under the License.
*/ */
import * as os from 'os';
import { TimeoutSettings } from '../common/timeoutSettings'; import { TimeoutSettings } from '../common/timeoutSettings';
import { createGuid, debugMode } from '../utils'; import { createGuid, debugMode } from '../utils';
import { mkdirIfNeeded } from '../utils/fileUtils'; import { mkdirIfNeeded } from '../utils/fileUtils';
@ -44,6 +43,7 @@ import { BrowserContextAPIRequestContext } from './fetch';
import type { Artifact } from './artifact'; import type { Artifact } from './artifact';
import { Clock } from './clock'; import { Clock } from './clock';
import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
import { RecorderApp } from './recorder/recorderApp';
export abstract class BrowserContext extends SdkObject { export abstract class BrowserContext extends SdkObject {
static Events = { static Events = {
@ -131,19 +131,21 @@ export abstract class BrowserContext extends SdkObject {
// When PWDEBUG=1, show inspector for each context. // When PWDEBUG=1, show inspector for each context.
if (debugMode() === 'inspector') if (debugMode() === 'inspector')
await Recorder.show(this, { pauseOnNextStatement: true }); await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true });
// When paused, show inspector. // When paused, show inspector.
if (this._debugger.isPaused()) if (this._debugger.isPaused())
Recorder.showInspector(this); Recorder.showInspector(this, RecorderApp.factory(this));
this._debugger.on(Debugger.Events.PausedStateChanged, () => { this._debugger.on(Debugger.Events.PausedStateChanged, () => {
Recorder.showInspector(this); if (this._debugger.isPaused())
Recorder.showInspector(this, RecorderApp.factory(this));
}); });
if (debugMode() === 'console') if (debugMode() === 'console')
await this.extendInjectedScript(consoleApiSource.source); await this.extendInjectedScript(consoleApiSource.source);
if (this._options.serviceWorkers === 'block') if (this._options.serviceWorkers === 'block')
await this.addInitScript(`\nnavigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`); await this.addInitScript(`\nif (navigator.serviceWorker) navigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`);
if (this._options.permissions) if (this._options.permissions)
await this.grantPermissions(this._options.permissions); await this.grantPermissions(this._options.permissions);
@ -700,11 +702,8 @@ export function validateBrowserContextOptions(options: channels.BrowserNewContex
options.recordVideo.size!.width &= ~1; options.recordVideo.size!.width &= ~1;
options.recordVideo.size!.height &= ~1; options.recordVideo.size!.height &= ~1;
} }
if (options.proxy) { if (options.proxy)
if (!browserOptions.proxy && browserOptions.isChromium && os.platform() === 'win32')
throw new Error(`Browser needs to be launched with the global proxy. If all contexts override the proxy, global proxy will be never used and can be any string, for example "launch({ proxy: { server: 'http://per-context' } })"`);
options.proxy = normalizeProxySettings(options.proxy); options.proxy = normalizeProxySettings(options.proxy);
}
verifyGeolocation(options.geolocation); verifyGeolocation(options.geolocation);
} }

View file

@ -32,7 +32,7 @@ import { ProgressController } from './progress';
import type * as types from './types'; import type * as types from './types';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { DEFAULT_TIMEOUT, TimeoutSettings } from '../common/timeoutSettings'; import { DEFAULT_TIMEOUT, TimeoutSettings } from '../common/timeoutSettings';
import { debugMode } from '../utils'; import { debugMode, ManualPromise } from '../utils';
import { existsAsync } from '../utils/fileUtils'; import { existsAsync } from '../utils/fileUtils';
import { helper } from './helper'; import { helper } from './helper';
import { RecentLogsCollector } from '../utils/debugLogger'; import { RecentLogsCollector } from '../utils/debugLogger';
@ -44,14 +44,24 @@ export const kNoXServerRunningError = 'Looks like you launched a headed browser
'Set either \'headless: true\' or use \'xvfb-run <your-playwright-app>\' before running Playwright.\n\n<3 Playwright Team'; 'Set either \'headless: true\' or use \'xvfb-run <your-playwright-app>\' before running Playwright.\n\n<3 Playwright Team';
export interface BrowserReadyState { export abstract class BrowserReadyState {
onBrowserOutput(message: string): void; protected readonly _wsEndpoint = new ManualPromise<string|undefined>();
onBrowserExit(): void;
waitUntilReady(): Promise<{ wsEndpoint?: string }>; onBrowserExit(): void {
// Unblock launch when browser prematurely exits.
this._wsEndpoint.resolve(undefined);
}
async waitUntilReady(): Promise<{ wsEndpoint?: string }> {
const wsEndpoint = await this._wsEndpoint;
return { wsEndpoint };
}
abstract onBrowserOutput(message: string): void;
} }
export abstract class BrowserType extends SdkObject { export abstract class BrowserType extends SdkObject {
private _name: BrowserName; private _name: BrowserName;
_useBidi: boolean = false;
constructor(parent: SdkObject, browserName: BrowserName) { constructor(parent: SdkObject, browserName: BrowserName) {
super(parent, 'browser-type'); super(parent, 'browser-type');
@ -69,6 +79,8 @@ export abstract class BrowserType extends SdkObject {
async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise<Browser> { async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise<Browser> {
options = this._validateLaunchOptions(options); options = this._validateLaunchOptions(options);
if (this._useBidi)
options.useWebSocket = true;
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
controller.setLogName('browser'); controller.setLogName('browser');
const browser = await controller.run(progress => { const browser = await controller.run(progress => {
@ -82,6 +94,8 @@ export abstract class BrowserType extends SdkObject {
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise<BrowserContext> { async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise<BrowserContext> {
options = this._validateLaunchOptions(options); options = this._validateLaunchOptions(options);
if (this._useBidi)
options.useWebSocket = true;
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
const persistent: channels.BrowserNewContextParams = { ...options }; const persistent: channels.BrowserNewContextParams = { ...options };
controller.setLogName('browser'); controller.setLogName('browser');

View file

@ -24,7 +24,7 @@ import type { Env } from '../../utils/processLauncher';
import { gracefullyCloseSet } from '../../utils/processLauncher'; import { gracefullyCloseSet } from '../../utils/processLauncher';
import { kBrowserCloseMessageId } from './crConnection'; import { kBrowserCloseMessageId } from './crConnection';
import { BrowserType, kNoXServerRunningError } from '../browserType'; import { BrowserType, kNoXServerRunningError } from '../browserType';
import type { BrowserReadyState } from '../browserType'; import { BrowserReadyState } from '../browserType';
import type { ConnectionTransport, ProtocolRequest } from '../transport'; import type { ConnectionTransport, ProtocolRequest } from '../transport';
import { WebSocketTransport } from '../transport'; import { WebSocketTransport } from '../transport';
import { CRDevTools } from './crDevTools'; import { CRDevTools } from './crDevTools';
@ -110,12 +110,6 @@ export class Chromium extends BrowserType {
artifactsDir, artifactsDir,
downloadsPath: options.downloadsPath || artifactsDir, downloadsPath: options.downloadsPath || artifactsDir,
tracesDir: options.tracesDir || artifactsDir, tracesDir: options.tracesDir || artifactsDir,
// On Windows context level proxies only work, if there isn't a global proxy
// set. This is currently a bug in the CR/Windows networking stack. By
// passing an arbitrary value we disable the check in PW land which warns
// users in normal (launch/launchServer) mode since otherwise connectOverCDP
// does not work at all with proxies on Windows.
proxy: { server: 'per-context' },
originalLaunchOptions: {}, originalLaunchOptions: {},
}; };
validateBrowserContextOptions(persistent, browserOptions); validateBrowserContextOptions(persistent, browserOptions);
@ -358,21 +352,12 @@ export class Chromium extends BrowserType {
} }
} }
class ChromiumReadyState implements BrowserReadyState { class ChromiumReadyState extends BrowserReadyState {
private readonly _wsEndpoint = new ManualPromise<string|undefined>(); override onBrowserOutput(message: string): void {
onBrowserOutput(message: string): void {
const match = message.match(/DevTools listening on (.*)/); const match = message.match(/DevTools listening on (.*)/);
if (match) if (match)
this._wsEndpoint.resolve(match[1]); this._wsEndpoint.resolve(match[1]);
} }
onBrowserExit(): void {
this._wsEndpoint.resolve(undefined);
}
async waitUntilReady(): Promise<{ wsEndpoint?: string }> {
const wsEndpoint = await this._wsEndpoint;
return { wsEndpoint };
}
} }
async function urlToWSEndpoint(progress: Progress, endpointURL: string, headers: { [key: string]: string; }) { async function urlToWSEndpoint(progress: Progress, endpointURL: string, headers: { [key: string]: string; }) {

View file

@ -609,7 +609,7 @@ class RouteImpl implements network.RouteDelegate {
this._interceptionId = interceptionId; this._interceptionId = interceptionId;
} }
async continue(request: network.Request, overrides: types.NormalizedContinueOverrides): Promise<void> { async continue(overrides: types.NormalizedContinueOverrides): Promise<void> {
this._alreadyContinuedParams = { this._alreadyContinuedParams = {
requestId: this._interceptionId!, requestId: this._interceptionId!,
url: overrides.url, url: overrides.url,

View file

@ -690,15 +690,16 @@ class FrameSession {
if (!frame || this._eventBelongsToStaleFrame(frame._id)) if (!frame || this._eventBelongsToStaleFrame(frame._id))
return; return;
const delegate = new CRExecutionContext(this._client, contextPayload); const delegate = new CRExecutionContext(this._client, contextPayload);
let worldName: types.World|null = null; let worldName: types.World;
if (contextPayload.auxData && !!contextPayload.auxData.isDefault) if (contextPayload.auxData && !!contextPayload.auxData.isDefault)
worldName = 'main'; worldName = 'main';
else if (contextPayload.name === UTILITY_WORLD_NAME) else if (contextPayload.name === UTILITY_WORLD_NAME)
worldName = 'utility'; worldName = 'utility';
else
return;
const context = new dom.FrameExecutionContext(delegate, frame, worldName); const context = new dom.FrameExecutionContext(delegate, frame, worldName);
(context as any)[contextDelegateSymbol] = delegate; (context as any)[contextDelegateSymbol] = delegate;
if (worldName) frame._contextCreated(worldName, context);
frame._contextCreated(worldName, context);
this._contextIdToContext.set(contextPayload.id, context); this._contextIdToContext.set(contextPayload.id, context);
} }
@ -897,7 +898,7 @@ class FrameSession {
const buffer = Buffer.from(payload.data, 'base64'); const buffer = Buffer.from(payload.data, 'base64');
this._page.emit(Page.Events.ScreencastFrame, { this._page.emit(Page.Events.ScreencastFrame, {
buffer, buffer,
timestamp: payload.metadata.timestamp, frameSwapWallTime: payload.metadata.timestamp ? payload.metadata.timestamp * 1000 : undefined,
width: payload.metadata.deviceWidth, width: payload.metadata.deviceWidth,
height: payload.metadata.deviceHeight, height: payload.metadata.deviceHeight,
}); });

View file

@ -1131,17 +1131,21 @@ using Audits.issueAdded event.
} }
/** /**
* Defines commands and events for browser extensions. Available if the client * Defines commands and events for browser extensions.
is connected using the --remote-debugging-pipe flag and
the --enable-unsafe-extension-debugging flag is set.
*/ */
export module Extensions { export module Extensions {
/**
* Storage areas.
*/
export type StorageArea = "session"|"local"|"sync"|"managed";
/** /**
* Installs an unpacked extension from the filesystem similar to * Installs an unpacked extension from the filesystem similar to
--load-extension CLI flags. Returns extension ID once the extension --load-extension CLI flags. Returns extension ID once the extension
has been installed. has been installed. Available if the client is connected using the
--remote-debugging-pipe flag and the --enable-unsafe-extension-debugging
flag is set.
*/ */
export type loadUnpackedParameters = { export type loadUnpackedParameters = {
/** /**
@ -1155,6 +1159,81 @@ has been installed.
*/ */
id: string; id: string;
} }
/**
* Gets data from extension storage in the given `storageArea`. If `keys` is
specified, these are used to filter the result.
*/
export type getStorageItemsParameters = {
/**
* ID of extension.
*/
id: string;
/**
* StorageArea to retrieve data from.
*/
storageArea: StorageArea;
/**
* Keys to retrieve.
*/
keys?: string[];
}
export type getStorageItemsReturnValue = {
data: { [key: string]: string };
}
/**
* Removes `keys` from extension storage in the given `storageArea`.
*/
export type removeStorageItemsParameters = {
/**
* ID of extension.
*/
id: string;
/**
* StorageArea to remove data from.
*/
storageArea: StorageArea;
/**
* Keys to remove.
*/
keys: string[];
}
export type removeStorageItemsReturnValue = {
}
/**
* Clears extension storage in the given `storageArea`.
*/
export type clearStorageItemsParameters = {
/**
* ID of extension.
*/
id: string;
/**
* StorageArea to remove data from.
*/
storageArea: StorageArea;
}
export type clearStorageItemsReturnValue = {
}
/**
* Sets `values` in extension storage in the given `storageArea`. The provided `values`
will be merged with existing values in the storage area.
*/
export type setStorageItemsParameters = {
/**
* ID of extension.
*/
id: string;
/**
* StorageArea to set data in.
*/
storageArea: StorageArea;
/**
* Values to set.
*/
values: { [key: string]: string };
}
export type setStorageItemsReturnValue = {
}
} }
/** /**
@ -2532,16 +2611,6 @@ stylesheet rules) this rule came from.
*/ */
style: CSSStyle; style: CSSStyle;
} }
/**
* CSS position-fallback rule representation.
*/
export interface CSSPositionFallbackRule {
name: Value;
/**
* List of keyframes.
*/
tryRules: CSSTryRule[];
}
/** /**
* CSS @position-try rule representation. * CSS @position-try rule representation.
*/ */
@ -2888,10 +2957,6 @@ attributes) for a DOM node identified by `nodeId`.
* A list of CSS keyframed animations matching this node. * A list of CSS keyframed animations matching this node.
*/ */
cssKeyframesRules?: CSSKeyframesRule[]; cssKeyframesRules?: CSSKeyframesRule[];
/**
* A list of CSS position fallbacks matching this node.
*/
cssPositionFallbackRules?: CSSPositionFallbackRule[];
/** /**
* A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property. * A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property.
*/ */
@ -3496,7 +3561,7 @@ front-end.
/** /**
* Pseudo element type. * Pseudo element type.
*/ */
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new";
/** /**
* Shadow root type. * Shadow root type.
*/ */
@ -3646,6 +3711,13 @@ The property is always undefined now.
compatibilityMode?: CompatibilityMode; compatibilityMode?: CompatibilityMode;
assignedSlot?: BackendNode; assignedSlot?: BackendNode;
} }
/**
* A structure to hold the top-level node of a detached tree and an array of its retained descendants.
*/
export interface DetachedElementInfo {
treeNode: Node;
retainedNodeIds: NodeId[];
}
/** /**
* A structure holding an RGBA color. * A structure holding an RGBA color.
*/ */
@ -4693,6 +4765,17 @@ File wrapper.
export type getFileInfoReturnValue = { export type getFileInfoReturnValue = {
path: string; path: string;
} }
/**
* Returns list of detached nodes
*/
export type getDetachedDomNodesParameters = {
}
export type getDetachedDomNodesReturnValue = {
/**
* The list of detached nodes
*/
detachedNodes: DetachedElementInfo[];
}
/** /**
* Enables console to refer to the node with given id via $x (see Command Line API for more details * Enables console to refer to the node with given id via $x (see Command Line API for more details
$x functions). $x functions).
@ -11369,7 +11452,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined * All Permissions Policy features. This enum should match the one defined
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
*/ */
export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
/** /**
* Reason for a permissions policy feature to be disabled. * Reason for a permissions policy feature to be disabled.
*/ */
@ -11784,7 +11867,7 @@ Example URLs: http://www.google.com/file.html -> "google.com"
*/ */
fixed?: number; fixed?: number;
} }
export type ClientNavigationReason = "formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"scriptInitiated"|"metaTagRefresh"|"pageBlockInterstitial"|"reload"|"anchorClick"; export type ClientNavigationReason = "anchorClick"|"formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"initialFrameNavigation"|"metaTagRefresh"|"other"|"pageBlockInterstitial"|"reload"|"scriptInitiated";
export type ClientNavigationDisposition = "currentTab"|"newTab"|"newWindow"|"download"; export type ClientNavigationDisposition = "currentTab"|"newTab"|"newWindow"|"download";
export interface InstallabilityErrorArgument { export interface InstallabilityErrorArgument {
/** /**
@ -12298,6 +12381,10 @@ when bfcache navigation fails.
* Frame's new url. * Frame's new url.
*/ */
url: string; url: string;
/**
* Navigation type
*/
navigationType: "fragment"|"historyApi"|"other";
} }
/** /**
* Compressed image data requested by the `startScreencast`. * Compressed image data requested by the `startScreencast`.
@ -16922,7 +17009,7 @@ possible for multiple rule sets and links to trigger a single attempt.
/** /**
* List of FinalStatus reasons for Prerender2. * List of FinalStatus reasons for Prerender2.
*/ */
export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"; export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated";
/** /**
* Preloading status values, see also PreloadingTriggeringOutcome. This * Preloading status values, see also PreloadingTriggeringOutcome. This
status is shared by prefetchStatusUpdated and prerenderStatusUpdated. status is shared by prefetchStatusUpdated and prerenderStatusUpdated.
@ -17270,6 +17357,101 @@ supported yet.
} }
} }
/**
* This domain allows configuring virtual Bluetooth devices to test
the web-bluetooth API.
*/
export module BluetoothEmulation {
/**
* Indicates the various states of Central.
*/
export type CentralState = "absent"|"powered-off"|"powered-on";
/**
* Stores the manufacturer data
*/
export interface ManufacturerData {
/**
* Company identifier
https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml
https://usb.org/developers
*/
key: number;
/**
* Manufacturer-specific data
*/
data: binary;
}
/**
* Stores the byte data of the advertisement packet sent by a Bluetooth device.
*/
export interface ScanRecord {
name?: string;
uuids?: string[];
/**
* Stores the external appearance description of the device.
*/
appearance?: number;
/**
* Stores the transmission power of a broadcasting device.
*/
txPower?: number;
/**
* Key is the company identifier and the value is an array of bytes of
manufacturer specific data.
*/
manufacturerData?: ManufacturerData[];
}
/**
* Stores the advertisement packet information that is sent by a Bluetooth device.
*/
export interface ScanEntry {
deviceAddress: string;
rssi: number;
scanRecord: ScanRecord;
}
/**
* Enable the BluetoothEmulation domain.
*/
export type enableParameters = {
/**
* State of the simulated central.
*/
state: CentralState;
}
export type enableReturnValue = {
}
/**
* Disable the BluetoothEmulation domain.
*/
export type disableParameters = {
}
export type disableReturnValue = {
}
/**
* Simulates a peripheral with |address|, |name| and |knownServiceUuids|
that has already been connected to the system.
*/
export type simulatePreconnectedPeripheralParameters = {
address: string;
name: string;
manufacturerData: ManufacturerData[];
knownServiceUuids: string[];
}
export type simulatePreconnectedPeripheralReturnValue = {
}
/**
* Simulates an advertisement packet described in |entry| being received by
the central.
*/
export type simulateAdvertisementParameters = {
entry: ScanEntry;
}
export type simulateAdvertisementReturnValue = {
}
}
/** /**
* This domain is deprecated - use Runtime or Log instead. * This domain is deprecated - use Runtime or Log instead.
*/ */
@ -20122,6 +20304,10 @@ Error was thrown.
"Audits.checkContrast": Audits.checkContrastParameters; "Audits.checkContrast": Audits.checkContrastParameters;
"Audits.checkFormsIssues": Audits.checkFormsIssuesParameters; "Audits.checkFormsIssues": Audits.checkFormsIssuesParameters;
"Extensions.loadUnpacked": Extensions.loadUnpackedParameters; "Extensions.loadUnpacked": Extensions.loadUnpackedParameters;
"Extensions.getStorageItems": Extensions.getStorageItemsParameters;
"Extensions.removeStorageItems": Extensions.removeStorageItemsParameters;
"Extensions.clearStorageItems": Extensions.clearStorageItemsParameters;
"Extensions.setStorageItems": Extensions.setStorageItemsParameters;
"Autofill.trigger": Autofill.triggerParameters; "Autofill.trigger": Autofill.triggerParameters;
"Autofill.setAddresses": Autofill.setAddressesParameters; "Autofill.setAddresses": Autofill.setAddressesParameters;
"Autofill.disable": Autofill.disableParameters; "Autofill.disable": Autofill.disableParameters;
@ -20232,6 +20418,7 @@ Error was thrown.
"DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledParameters; "DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledParameters;
"DOM.getNodeStackTraces": DOM.getNodeStackTracesParameters; "DOM.getNodeStackTraces": DOM.getNodeStackTracesParameters;
"DOM.getFileInfo": DOM.getFileInfoParameters; "DOM.getFileInfo": DOM.getFileInfoParameters;
"DOM.getDetachedDomNodes": DOM.getDetachedDomNodesParameters;
"DOM.setInspectedNode": DOM.setInspectedNodeParameters; "DOM.setInspectedNode": DOM.setInspectedNodeParameters;
"DOM.setNodeName": DOM.setNodeNameParameters; "DOM.setNodeName": DOM.setNodeNameParameters;
"DOM.setNodeValue": DOM.setNodeValueParameters; "DOM.setNodeValue": DOM.setNodeValueParameters;
@ -20616,6 +20803,10 @@ Error was thrown.
"PWA.launchFilesInApp": PWA.launchFilesInAppParameters; "PWA.launchFilesInApp": PWA.launchFilesInAppParameters;
"PWA.openCurrentPageInApp": PWA.openCurrentPageInAppParameters; "PWA.openCurrentPageInApp": PWA.openCurrentPageInAppParameters;
"PWA.changeAppUserSettings": PWA.changeAppUserSettingsParameters; "PWA.changeAppUserSettings": PWA.changeAppUserSettingsParameters;
"BluetoothEmulation.enable": BluetoothEmulation.enableParameters;
"BluetoothEmulation.disable": BluetoothEmulation.disableParameters;
"BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralParameters;
"BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementParameters;
"Console.clearMessages": Console.clearMessagesParameters; "Console.clearMessages": Console.clearMessagesParameters;
"Console.disable": Console.disableParameters; "Console.disable": Console.disableParameters;
"Console.enable": Console.enableParameters; "Console.enable": Console.enableParameters;
@ -20722,6 +20913,10 @@ Error was thrown.
"Audits.checkContrast": Audits.checkContrastReturnValue; "Audits.checkContrast": Audits.checkContrastReturnValue;
"Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue; "Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue;
"Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue; "Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue;
"Extensions.getStorageItems": Extensions.getStorageItemsReturnValue;
"Extensions.removeStorageItems": Extensions.removeStorageItemsReturnValue;
"Extensions.clearStorageItems": Extensions.clearStorageItemsReturnValue;
"Extensions.setStorageItems": Extensions.setStorageItemsReturnValue;
"Autofill.trigger": Autofill.triggerReturnValue; "Autofill.trigger": Autofill.triggerReturnValue;
"Autofill.setAddresses": Autofill.setAddressesReturnValue; "Autofill.setAddresses": Autofill.setAddressesReturnValue;
"Autofill.disable": Autofill.disableReturnValue; "Autofill.disable": Autofill.disableReturnValue;
@ -20832,6 +21027,7 @@ Error was thrown.
"DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledReturnValue; "DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledReturnValue;
"DOM.getNodeStackTraces": DOM.getNodeStackTracesReturnValue; "DOM.getNodeStackTraces": DOM.getNodeStackTracesReturnValue;
"DOM.getFileInfo": DOM.getFileInfoReturnValue; "DOM.getFileInfo": DOM.getFileInfoReturnValue;
"DOM.getDetachedDomNodes": DOM.getDetachedDomNodesReturnValue;
"DOM.setInspectedNode": DOM.setInspectedNodeReturnValue; "DOM.setInspectedNode": DOM.setInspectedNodeReturnValue;
"DOM.setNodeName": DOM.setNodeNameReturnValue; "DOM.setNodeName": DOM.setNodeNameReturnValue;
"DOM.setNodeValue": DOM.setNodeValueReturnValue; "DOM.setNodeValue": DOM.setNodeValueReturnValue;
@ -21216,6 +21412,10 @@ Error was thrown.
"PWA.launchFilesInApp": PWA.launchFilesInAppReturnValue; "PWA.launchFilesInApp": PWA.launchFilesInAppReturnValue;
"PWA.openCurrentPageInApp": PWA.openCurrentPageInAppReturnValue; "PWA.openCurrentPageInApp": PWA.openCurrentPageInAppReturnValue;
"PWA.changeAppUserSettings": PWA.changeAppUserSettingsReturnValue; "PWA.changeAppUserSettings": PWA.changeAppUserSettingsReturnValue;
"BluetoothEmulation.enable": BluetoothEmulation.enableReturnValue;
"BluetoothEmulation.disable": BluetoothEmulation.disableReturnValue;
"BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralReturnValue;
"BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementReturnValue;
"Console.clearMessages": Console.clearMessagesReturnValue; "Console.clearMessages": Console.clearMessagesReturnValue;
"Console.disable": Console.disableReturnValue; "Console.disable": Console.disableReturnValue;
"Console.enable": Console.enableReturnValue; "Console.enable": Console.enableReturnValue;

View file

@ -53,7 +53,7 @@ export class VideoRecorder {
private constructor(page: Page, ffmpegPath: string, progress: Progress) { private constructor(page: Page, ffmpegPath: string, progress: Progress) {
this._progress = progress; this._progress = progress;
this._ffmpegPath = ffmpegPath; this._ffmpegPath = ffmpegPath;
page.on(Page.Events.ScreencastFrame, frame => this.writeFrame(frame.buffer, frame.timestamp)); page.on(Page.Events.ScreencastFrame, frame => this.writeFrame(frame.buffer, frame.frameSwapWallTime / 1000));
} }
private async _launch(options: types.PageScreencastOptions) { private async _launch(options: types.PageScreencastOptions) {

View file

@ -0,0 +1,3 @@
[*]
../../utils/
../deviceDescriptors.ts

View file

@ -14,13 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../../types/types';
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
import { sanitizeDeviceOptions, toSignalMap } from './language'; import { sanitizeDeviceOptions, toClickOptionsForSourceCode, toKeyboardModifiers, toSignalMap } from './language';
import type { ActionInContext } from './codeGenerator';
import type { Action } from './recorderActions';
import type { MouseClickOptions } from './utils';
import { toModifiers } from './utils';
import { escapeWithQuotes, asLocator } from '../../utils'; import { escapeWithQuotes, asLocator } from '../../utils';
import { deviceDescriptors } from '../deviceDescriptors'; import { deviceDescriptors } from '../deviceDescriptors';
@ -72,14 +68,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
let subject: string; const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.ContentFrame()`);
if (actionInContext.frame.isMainFrame) { const subject = `${pageAlias}${locators.join('')}`;
subject = pageAlias;
} else {
const locators = actionInContext.frame.selectorsChain.map(selector => `.FrameLocator(${quote(selector)})`);
subject = `${pageAlias}${locators.join('')}`;
}
const signals = toSignalMap(action); const signals = toSignalMap(action);
if (signals.dialog) { if (signals.dialog) {
@ -93,7 +83,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
} }
const lines: string[] = []; const lines: string[] = [];
lines.push(this._generateActionCall(subject, action)); lines.push(this._generateActionCall(subject, actionInContext));
if (signals.download) { if (signals.download) {
lines.unshift(`var download${signals.download.downloadAlias} = await ${pageAlias}.RunAndWaitForDownloadAsync(async () =>\n{`); lines.unshift(`var download${signals.download.downloadAlias} = await ${pageAlias}.RunAndWaitForDownloadAsync(async () =>\n{`);
@ -111,7 +101,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
private _generateActionCall(subject: string, action: Action): string { private _generateActionCall(subject: string, actionInContext: ActionInContext): string {
const action = actionInContext.action;
switch (action.name) { switch (action.name) {
case 'openPage': case 'openPage':
throw Error('Not reached'); throw Error('Not reached');
@ -121,16 +112,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
let method = 'Click'; let method = 'Click';
if (action.clickCount === 2) if (action.clickCount === 2)
method = 'DblClick'; method = 'DblClick';
const modifiers = toModifiers(action.modifiers); const options = toClickOptionsForSourceCode(action);
const options: MouseClickOptions = {};
if (action.button !== 'left')
options.button = action.button;
if (modifiers.length)
options.modifiers = modifiers;
if (action.clickCount > 2)
options.clickCount = action.clickCount;
if (action.position)
options.position = action.position;
if (!Object.entries(options).length) if (!Object.entries(options).length)
return `await ${subject}.${this._asLocator(action.selector)}.${method}Async();`; return `await ${subject}.${this._asLocator(action.selector)}.${method}Async();`;
const optionsString = formatObject(options, ' ', 'Locator' + method + 'Options'); const optionsString = formatObject(options, ' ', 'Locator' + method + 'Options');
@ -145,7 +127,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
case 'setInputFiles': case 'setInputFiles':
return `await ${subject}.${this._asLocator(action.selector)}.SetInputFilesAsync(${formatObject(action.files)});`; return `await ${subject}.${this._asLocator(action.selector)}.SetInputFilesAsync(${formatObject(action.files)});`;
case 'press': { case 'press': {
const modifiers = toModifiers(action.modifiers); const modifiers = toKeyboardModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+'); const shortcut = [...modifiers, action.key].join('+');
return `await ${subject}.${this._asLocator(action.selector)}.PressAsync(${quote(shortcut)});`; return `await ${subject}.${this._asLocator(action.selector)}.PressAsync(${quote(shortcut)});`;
} }

View file

@ -14,13 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../../types/types';
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; import type * as types from '../types';
import { toSignalMap } from './language'; import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
import type { ActionInContext } from './codeGenerator'; import { toClickOptionsForSourceCode, toKeyboardModifiers, toSignalMap } from './language';
import type { Action } from './recorderActions';
import type { MouseClickOptions } from './utils';
import { toModifiers } from './utils';
import { deviceDescriptors } from '../deviceDescriptors'; import { deviceDescriptors } from '../deviceDescriptors';
import { JavaScriptFormatter } from './javascript'; import { JavaScriptFormatter } from './javascript';
import { escapeWithQuotes, asLocator } from '../../utils'; import { escapeWithQuotes, asLocator } from '../../utils';
@ -63,16 +60,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
let subject: string; const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector, false)}.contentFrame()`);
let inFrameLocator = false; const subject = `${pageAlias}${locators.join('')}`;
if (actionInContext.frame.isMainFrame) {
subject = pageAlias;
} else {
const locators = actionInContext.frame.selectorsChain.map(selector => `.frameLocator(${quote(selector)})`);
subject = `${pageAlias}${locators.join('')}`;
inFrameLocator = true;
}
const signals = toSignalMap(action); const signals = toSignalMap(action);
if (signals.dialog) { if (signals.dialog) {
@ -82,7 +71,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
});`); });`);
} }
let code = this._generateActionCall(subject, action, inFrameLocator); let code = this._generateActionCall(subject, actionInContext, !!actionInContext.frame.framePath.length);
if (signals.popup) { if (signals.popup) {
code = `Page ${signals.popup.popupAlias} = ${pageAlias}.waitForPopup(() -> { code = `Page ${signals.popup.popupAlias} = ${pageAlias}.waitForPopup(() -> {
@ -101,7 +90,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
private _generateActionCall(subject: string, action: Action, inFrameLocator: boolean): string { private _generateActionCall(subject: string, actionInContext: ActionInContext, inFrameLocator: boolean): string {
const action = actionInContext.action;
switch (action.name) { switch (action.name) {
case 'openPage': case 'openPage':
throw Error('Not reached'); throw Error('Not reached');
@ -111,16 +101,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
let method = 'click'; let method = 'click';
if (action.clickCount === 2) if (action.clickCount === 2)
method = 'dblclick'; method = 'dblclick';
const modifiers = toModifiers(action.modifiers); const options = toClickOptionsForSourceCode(action);
const options: MouseClickOptions = {};
if (action.button !== 'left')
options.button = action.button;
if (modifiers.length)
options.modifiers = modifiers;
if (action.clickCount > 2)
options.clickCount = action.clickCount;
if (action.position)
options.position = action.position;
const optionsText = formatClickOptions(options); const optionsText = formatClickOptions(options);
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.${method}(${optionsText});`; return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.${method}(${optionsText});`;
} }
@ -133,7 +114,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
case 'setInputFiles': case 'setInputFiles':
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.setInputFiles(${formatPath(action.files.length === 1 ? action.files[0] : action.files)});`; return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.setInputFiles(${formatPath(action.files.length === 1 ? action.files[0] : action.files)});`;
case 'press': { case 'press': {
const modifiers = toModifiers(action.modifiers); const modifiers = toKeyboardModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+'); const shortcut = [...modifiers, action.key].join('+');
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.press(${quote(shortcut)});`; return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.press(${quote(shortcut)});`;
} }
@ -279,7 +260,7 @@ function formatContextOptions(contextOptions: BrowserContextOptions, deviceName:
return lines.join('\n'); return lines.join('\n');
} }
function formatClickOptions(options: MouseClickOptions) { function formatClickOptions(options: types.MouseClickOptions) {
const lines = []; const lines = [];
if (options.button) if (options.button)
lines.push(` .setButton(MouseButton.${options.button.toUpperCase()})`); lines.push(` .setButton(MouseButton.${options.button.toUpperCase()})`);

View file

@ -14,13 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../../types/types';
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
import { sanitizeDeviceOptions, toSignalMap } from './language'; import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptionsForSourceCode } from './language';
import type { ActionInContext } from './codeGenerator';
import type { Action } from './recorderActions';
import type { MouseClickOptions } from './utils';
import { toModifiers } from './utils';
import { deviceDescriptors } from '../deviceDescriptors'; import { deviceDescriptors } from '../deviceDescriptors';
import { escapeWithQuotes, asLocator } from '../../utils'; import { escapeWithQuotes, asLocator } from '../../utils';
@ -52,14 +48,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
let subject: string; const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.contentFrame()`);
if (actionInContext.frame.isMainFrame) { const subject = `${pageAlias}${locators.join('')}`;
subject = pageAlias;
} else {
const locators = actionInContext.frame.selectorsChain.map(selector => `.frameLocator(${quote(selector)})`);
subject = `${pageAlias}${locators.join('')}`;
}
const signals = toSignalMap(action); const signals = toSignalMap(action);
if (signals.dialog) { if (signals.dialog) {
@ -74,7 +64,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
if (signals.download) if (signals.download)
formatter.add(`const download${signals.download.downloadAlias}Promise = ${pageAlias}.waitForEvent('download');`); formatter.add(`const download${signals.download.downloadAlias}Promise = ${pageAlias}.waitForEvent('download');`);
formatter.add(this._generateActionCall(subject, action)); formatter.add(wrapWithStep(actionInContext.description, this._generateActionCall(subject, actionInContext)));
if (signals.popup) if (signals.popup)
formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`); formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`);
@ -84,7 +74,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
private _generateActionCall(subject: string, action: Action): string { private _generateActionCall(subject: string, actionInContext: ActionInContext): string {
const action = actionInContext.action;
switch (action.name) { switch (action.name) {
case 'openPage': case 'openPage':
throw Error('Not reached'); throw Error('Not reached');
@ -94,16 +85,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
let method = 'click'; let method = 'click';
if (action.clickCount === 2) if (action.clickCount === 2)
method = 'dblclick'; method = 'dblclick';
const modifiers = toModifiers(action.modifiers); const options = toClickOptionsForSourceCode(action);
const options: MouseClickOptions = {};
if (action.button !== 'left')
options.button = action.button;
if (modifiers.length)
options.modifiers = modifiers;
if (action.clickCount > 2)
options.clickCount = action.clickCount;
if (action.position)
options.position = action.position;
const optionsString = formatOptions(options, false); const optionsString = formatOptions(options, false);
return `await ${subject}.${this._asLocator(action.selector)}.${method}(${optionsString});`; return `await ${subject}.${this._asLocator(action.selector)}.${method}(${optionsString});`;
} }
@ -116,7 +98,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
case 'setInputFiles': case 'setInputFiles':
return `await ${subject}.${this._asLocator(action.selector)}.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)});`; return `await ${subject}.${this._asLocator(action.selector)}.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)});`;
case 'press': { case 'press': {
const modifiers = toModifiers(action.modifiers); const modifiers = toKeyboardModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+'); const shortcut = [...modifiers, action.key].join('+');
return `await ${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)});`; return `await ${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)});`;
} }
@ -276,3 +258,9 @@ export class JavaScriptFormatter {
function quote(text: string) { function quote(text: string) {
return escapeWithQuotes(text, '\''); return escapeWithQuotes(text, '\'');
} }
function wrapWithStep(description: string | undefined, body: string) {
return description ? `await test.step(\`${description}\`, async () => {
${body}
});` : body;
}

View file

@ -15,8 +15,7 @@
*/ */
import { asLocator } from '../../utils'; import { asLocator } from '../../utils';
import type { ActionInContext } from './codeGenerator'; import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
export class JsonlLanguageGenerator implements LanguageGenerator { export class JsonlLanguageGenerator implements LanguageGenerator {
id = 'jsonl'; id = 'jsonl';

View file

@ -0,0 +1,85 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { BrowserContextOptions } from '../../..';
import type * as actions from '../recorder/recorderActions';
import type * as types from '../types';
import type { ActionInContext, LanguageGenerator, LanguageGeneratorOptions } from './types';
export function generateCode(actions: ActionInContext[], languageGenerator: LanguageGenerator, options: LanguageGeneratorOptions) {
const header = languageGenerator.generateHeader(options);
const footer = languageGenerator.generateFooter(options.saveStorage);
const actionTexts = actions.map(a => languageGenerator.generateAction(a)).filter(Boolean);
const text = [header, ...actionTexts, footer].join('\n');
return { header, footer, actionTexts, text };
}
export function sanitizeDeviceOptions(device: any, options: BrowserContextOptions): BrowserContextOptions {
// Filter out all the properties from the device descriptor.
const cleanedOptions: Record<string, any> = {};
for (const property in options) {
if (JSON.stringify(device[property]) !== JSON.stringify((options as any)[property]))
cleanedOptions[property] = (options as any)[property];
}
return cleanedOptions;
}
export function toSignalMap(action: actions.Action) {
let popup: actions.PopupSignal | undefined;
let download: actions.DownloadSignal | undefined;
let dialog: actions.DialogSignal | undefined;
for (const signal of action.signals) {
if (signal.name === 'popup')
popup = signal;
else if (signal.name === 'download')
download = signal;
else if (signal.name === 'dialog')
dialog = signal;
}
return {
popup,
download,
dialog,
};
}
export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModifier[] {
const result: types.SmartKeyboardModifier[] = [];
if (modifiers & 1)
result.push('Alt');
if (modifiers & 2)
result.push('ControlOrMeta');
if (modifiers & 4)
result.push('ControlOrMeta');
if (modifiers & 8)
result.push('Shift');
return result;
}
export function toClickOptionsForSourceCode(action: actions.ClickAction): types.MouseClickOptions {
const modifiers = toKeyboardModifiers(action.modifiers);
const options: types.MouseClickOptions = {};
if (action.button !== 'left')
options.button = action.button;
if (modifiers.length)
options.modifiers = modifiers;
// Do not render clickCount === 2 for dblclick.
if (action.clickCount > 2)
options.clickCount = action.clickCount;
if (action.position)
options.position = action.position;
return options;
}

View file

@ -0,0 +1,37 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JavaLanguageGenerator } from './java';
import { JavaScriptLanguageGenerator } from './javascript';
import { JsonlLanguageGenerator } from './jsonl';
import { CSharpLanguageGenerator } from './csharp';
import { PythonLanguageGenerator } from './python';
export function languageSet() {
return new Set([
new JavaLanguageGenerator('junit'),
new JavaLanguageGenerator('library'),
new JavaScriptLanguageGenerator(/* isPlaywrightTest */false),
new JavaScriptLanguageGenerator(/* isPlaywrightTest */true),
new PythonLanguageGenerator(/* isAsync */false, /* isPytest */true),
new PythonLanguageGenerator(/* isAsync */false, /* isPytest */false),
new PythonLanguageGenerator(/* isAsync */true, /* isPytest */false),
new CSharpLanguageGenerator('mstest'),
new CSharpLanguageGenerator('nunit'),
new CSharpLanguageGenerator('library'),
new JsonlLanguageGenerator(),
]);
}

View file

@ -14,13 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../../types/types';
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
import { sanitizeDeviceOptions, toSignalMap } from './language'; import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptionsForSourceCode } from './language';
import type { ActionInContext } from './codeGenerator';
import type { Action } from './recorderActions';
import type { MouseClickOptions } from './utils';
import { toModifiers } from './utils';
import { escapeWithQuotes, toSnakeCase, asLocator } from '../../utils'; import { escapeWithQuotes, toSnakeCase, asLocator } from '../../utils';
import { deviceDescriptors } from '../deviceDescriptors'; import { deviceDescriptors } from '../deviceDescriptors';
@ -59,20 +55,14 @@ export class PythonLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
let subject: string; const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.content_frame()`);
if (actionInContext.frame.isMainFrame) { const subject = `${pageAlias}${locators.join('')}`;
subject = pageAlias;
} else {
const locators = actionInContext.frame.selectorsChain.map(selector => `.frame_locator(${quote(selector)})`);
subject = `${pageAlias}${locators.join('')}`;
}
const signals = toSignalMap(action); const signals = toSignalMap(action);
if (signals.dialog) if (signals.dialog)
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`); formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`);
let code = `${this._awaitPrefix}${this._generateActionCall(subject, action)}`; let code = `${this._awaitPrefix}${this._generateActionCall(subject, actionInContext)}`;
if (signals.popup) { if (signals.popup) {
code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info { code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info {
@ -93,7 +83,8 @@ export class PythonLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
private _generateActionCall(subject: string, action: Action): string { private _generateActionCall(subject: string, actionInContext: ActionInContext): string {
const action = actionInContext.action;
switch (action.name) { switch (action.name) {
case 'openPage': case 'openPage':
throw Error('Not reached'); throw Error('Not reached');
@ -103,16 +94,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
let method = 'click'; let method = 'click';
if (action.clickCount === 2) if (action.clickCount === 2)
method = 'dblclick'; method = 'dblclick';
const modifiers = toModifiers(action.modifiers); const options = toClickOptionsForSourceCode(action);
const options: MouseClickOptions = {};
if (action.button !== 'left')
options.button = action.button;
if (modifiers.length)
options.modifiers = modifiers;
if (action.clickCount > 2)
options.clickCount = action.clickCount;
if (action.position)
options.position = action.position;
const optionsString = formatOptions(options, false); const optionsString = formatOptions(options, false);
return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`; return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
} }
@ -125,7 +107,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
case 'setInputFiles': case 'setInputFiles':
return `${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`; return `${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
case 'press': { case 'press': {
const modifiers = toModifiers(action.modifiers); const modifiers = toKeyboardModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+'); const shortcut = [...modifiers, action.key].join('+');
return `${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`; return `${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`;
} }

View file

@ -0,0 +1,50 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { BrowserContextOptions, LaunchOptions } from '../../../types/types';
import type * as actions from '../recorder/recorderActions';
import type { Language } from '../../utils';
export type { Language } from '../../utils';
export type LanguageGeneratorOptions = {
browserName: string;
launchOptions: LaunchOptions;
contextOptions: BrowserContextOptions;
deviceName?: string;
saveStorage?: string;
};
export type FrameDescription = {
pageAlias: string;
framePath: string[];
};
export type ActionInContext = {
frame: FrameDescription;
description?: string;
action: actions.Action;
committed?: boolean;
};
export interface LanguageGenerator {
id: string;
groupName: string;
name: string;
highlighter: Language;
generateHeader(options: LanguageGeneratorOptions): string;
generateAction(actionInContext: ActionInContext): string;
generateFooter(saveStorage: string | undefined): string;
}

View file

@ -52,7 +52,6 @@ export class DebugController extends SdkObject {
initialize(codegenId: string, sdkLanguage: Language) { initialize(codegenId: string, sdkLanguage: Language) {
this._codegenId = codegenId; this._codegenId = codegenId;
this._sdkLanguage = sdkLanguage; this._sdkLanguage = sdkLanguage;
Recorder.setAppFactory(async () => new InspectingRecorderApp(this));
} }
setAutoCloseAllowed(allowed: boolean) { setAutoCloseAllowed(allowed: boolean) {
@ -62,7 +61,6 @@ export class DebugController extends SdkObject {
dispose() { dispose() {
this.setReportStateChanged(false); this.setReportStateChanged(false);
this.setAutoCloseAllowed(false); this.setAutoCloseAllowed(false);
Recorder.setAppFactory(undefined);
} }
setReportStateChanged(enabled: boolean) { setReportStateChanged(enabled: boolean) {
@ -199,7 +197,7 @@ export class DebugController extends SdkObject {
const contexts = new Set<BrowserContext>(); const contexts = new Set<BrowserContext>();
for (const page of this._playwright.allPages()) for (const page of this._playwright.allPages())
contexts.add(page.context()); contexts.add(page.context());
const result = await Promise.all([...contexts].map(c => Recorder.show(c, { omitCallTracking: true }))); const result = await Promise.all([...contexts].map(c => Recorder.show(c, () => Promise.resolve(new InspectingRecorderApp(this)), { omitCallTracking: true })));
return result.filter(Boolean) as Recorder[]; return result.filter(Boolean) as Recorder[];
} }

View file

@ -110,7 +110,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Galaxy S5": { "Galaxy S5": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -121,7 +121,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S5 landscape": { "Galaxy S5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -132,7 +132,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S8": { "Galaxy S8": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 740 "height": 740
@ -143,7 +143,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S8 landscape": { "Galaxy S8 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 740, "width": 740,
"height": 360 "height": 360
@ -154,7 +154,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S9+": { "Galaxy S9+": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 320, "width": 320,
"height": 658 "height": 658
@ -165,7 +165,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S9+ landscape": { "Galaxy S9+ landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 658, "width": 658,
"height": 320 "height": 320
@ -176,7 +176,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy Tab S4": { "Galaxy Tab S4": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Safari/537.36",
"viewport": { "viewport": {
"width": 712, "width": 712,
"height": 1138 "height": 1138
@ -187,7 +187,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy Tab S4 landscape": { "Galaxy Tab S4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Safari/537.36",
"viewport": { "viewport": {
"width": 1138, "width": 1138,
"height": 712 "height": 712
@ -1098,7 +1098,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"LG Optimus L70": { "LG Optimus L70": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 384, "width": 384,
"height": 640 "height": 640
@ -1109,7 +1109,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"LG Optimus L70 landscape": { "LG Optimus L70 landscape": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 384 "height": 384
@ -1120,7 +1120,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 550": { "Microsoft Lumia 550": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1131,7 +1131,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 550 landscape": { "Microsoft Lumia 550 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1142,7 +1142,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 950": { "Microsoft Lumia 950": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1153,7 +1153,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 950 landscape": { "Microsoft Lumia 950 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1164,7 +1164,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 10": { "Nexus 10": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Safari/537.36",
"viewport": { "viewport": {
"width": 800, "width": 800,
"height": 1280 "height": 1280
@ -1175,7 +1175,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 10 landscape": { "Nexus 10 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Safari/537.36",
"viewport": { "viewport": {
"width": 1280, "width": 1280,
"height": 800 "height": 800
@ -1186,7 +1186,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 4": { "Nexus 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 384, "width": 384,
"height": 640 "height": 640
@ -1197,7 +1197,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 4 landscape": { "Nexus 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 384 "height": 384
@ -1208,7 +1208,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5": { "Nexus 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1219,7 +1219,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5 landscape": { "Nexus 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1230,7 +1230,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5X": { "Nexus 5X": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1241,7 +1241,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5X landscape": { "Nexus 5X landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1252,7 +1252,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6": { "Nexus 6": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1263,7 +1263,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6 landscape": { "Nexus 6 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1274,7 +1274,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6P": { "Nexus 6P": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1285,7 +1285,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6P landscape": { "Nexus 6P landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1296,7 +1296,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 7": { "Nexus 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Safari/537.36",
"viewport": { "viewport": {
"width": 600, "width": 600,
"height": 960 "height": 960
@ -1307,7 +1307,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 7 landscape": { "Nexus 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Safari/537.36",
"viewport": { "viewport": {
"width": 960, "width": 960,
"height": 600 "height": 600
@ -1362,7 +1362,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Pixel 2": { "Pixel 2": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 411, "width": 411,
"height": 731 "height": 731
@ -1373,7 +1373,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 landscape": { "Pixel 2 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 731, "width": 731,
"height": 411 "height": 411
@ -1384,7 +1384,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 XL": { "Pixel 2 XL": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 411, "width": 411,
"height": 823 "height": 823
@ -1395,7 +1395,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 XL landscape": { "Pixel 2 XL landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 823, "width": 823,
"height": 411 "height": 411
@ -1406,7 +1406,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 3": { "Pixel 3": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 393, "width": 393,
"height": 786 "height": 786
@ -1417,7 +1417,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 3 landscape": { "Pixel 3 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 786, "width": 786,
"height": 393 "height": 393
@ -1428,7 +1428,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4": { "Pixel 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 353, "width": 353,
"height": 745 "height": 745
@ -1439,7 +1439,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4 landscape": { "Pixel 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 745, "width": 745,
"height": 353 "height": 353
@ -1450,7 +1450,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4a (5G)": { "Pixel 4a (5G)": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"screen": { "screen": {
"width": 412, "width": 412,
"height": 892 "height": 892
@ -1465,7 +1465,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4a (5G) landscape": { "Pixel 4a (5G) landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"screen": { "screen": {
"height": 892, "height": 892,
"width": 412 "width": 412
@ -1480,7 +1480,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 5": { "Pixel 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"screen": { "screen": {
"width": 393, "width": 393,
"height": 851 "height": 851
@ -1495,7 +1495,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 5 landscape": { "Pixel 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"screen": { "screen": {
"width": 851, "width": 851,
"height": 393 "height": 393
@ -1510,7 +1510,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 7": { "Pixel 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"screen": { "screen": {
"width": 412, "width": 412,
"height": 915 "height": 915
@ -1525,7 +1525,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 7 landscape": { "Pixel 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"screen": { "screen": {
"width": 915, "width": 915,
"height": 412 "height": 412
@ -1540,7 +1540,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Moto G4": { "Moto G4": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1551,7 +1551,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Moto G4 landscape": { "Moto G4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1562,7 +1562,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Chrome HiDPI": { "Desktop Chrome HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Safari/537.36",
"screen": { "screen": {
"width": 1792, "width": 1792,
"height": 1120 "height": 1120
@ -1577,7 +1577,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Edge HiDPI": { "Desktop Edge HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36 Edg/128.0.6613.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Safari/537.36 Edg/129.0.6668.42",
"screen": { "screen": {
"width": 1792, "width": 1792,
"height": 1120 "height": 1120
@ -1592,7 +1592,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Firefox HiDPI": { "Desktop Firefox HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0",
"screen": { "screen": {
"width": 1792, "width": 1792,
"height": 1120 "height": 1120
@ -1622,7 +1622,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Desktop Chrome": { "Desktop Chrome": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Safari/537.36",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080
@ -1637,7 +1637,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Edge": { "Desktop Edge": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36 Edg/128.0.6613.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.42 Safari/537.36 Edg/129.0.6668.42",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080
@ -1652,7 +1652,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Firefox": { "Desktop Firefox": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080

View file

@ -39,6 +39,8 @@ import type { Dialog } from '../dialog';
import type { ConsoleMessage } from '../console'; import type { ConsoleMessage } from '../console';
import { serializeError } from '../errors'; import { serializeError } from '../errors';
import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { ElementHandleDispatcher } from './elementHandlerDispatcher';
import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer';
import { RecorderApp } from '../recorder/recorderApp';
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel { export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
_type_EventTarget = true; _type_EventTarget = true;
@ -291,7 +293,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
} }
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> { async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
await Recorder.show(this._context, params); const factory = process.env.PW_RECORDER_IS_TRACE_VIEWER ? RecorderInTraceViewer.factory(this._context) : RecorderApp.factory(this._context);
await Recorder.show(this._context, factory, params);
} }
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) { async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {

View file

@ -46,6 +46,8 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
chromium: new BrowserTypeDispatcher(scope, playwright.chromium), chromium: new BrowserTypeDispatcher(scope, playwright.chromium),
firefox: new BrowserTypeDispatcher(scope, playwright.firefox), firefox: new BrowserTypeDispatcher(scope, playwright.firefox),
webkit: new BrowserTypeDispatcher(scope, playwright.webkit), webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
bidiChromium: new BrowserTypeDispatcher(scope, playwright.bidiChromium),
bidiFirefox: new BrowserTypeDispatcher(scope, playwright.bidiFirefox),
android, android,
electron: new ElectronDispatcher(scope, playwright.electron), electron: new ElectronDispatcher(scope, playwright.electron),
utils: playwright.options.isServer ? undefined : new LocalUtilsDispatcher(scope, playwright), utils: playwright.options.isServer ? undefined : new LocalUtilsDispatcher(scope, playwright),

View file

@ -50,9 +50,9 @@ export function isNonRecoverableDOMError(error: Error) {
export class FrameExecutionContext extends js.ExecutionContext { export class FrameExecutionContext extends js.ExecutionContext {
readonly frame: frames.Frame; readonly frame: frames.Frame;
private _injectedScriptPromise?: Promise<js.JSHandle>; private _injectedScriptPromise?: Promise<js.JSHandle>;
readonly world: types.World | null; readonly world: types.World;
constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame, world: types.World|null) { constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame, world: types.World) {
super(frame, delegate, world || 'content-script'); super(frame, delegate, world || 'content-script');
this.frame = frame; this.frame = frame;
this.world = world; this.world = world;
@ -233,7 +233,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
this._page._timeoutSettings.timeout(options)); this._page._timeoutSettings.timeout(options));
} }
private async _clickablePoint(): Promise<types.Point | 'error:notvisible' | 'error:notinviewport'> { private async _clickablePoint(): Promise<types.Point | 'error:notvisible' | 'error:notinviewport' | 'error:notconnected'> {
const intersectQuadWithViewport = (quad: types.Quad): types.Quad => { const intersectQuadWithViewport = (quad: types.Quad): types.Quad => {
return quad.map(point => ({ return quad.map(point => ({
x: Math.min(Math.max(point.x, 0), metrics.width), x: Math.min(Math.max(point.x, 0), metrics.width),
@ -257,6 +257,8 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
this._page._delegate.getContentQuads(this), this._page._delegate.getContentQuads(this),
this._page.mainFrame()._utilityContext().then(utility => utility.evaluate(() => ({ width: innerWidth, height: innerHeight }))), this._page.mainFrame()._utilityContext().then(utility => utility.evaluate(() => ({ width: innerWidth, height: innerHeight }))),
] as const); ] as const);
if (quads === 'error:notconnected')
return quads;
if (!quads || !quads.length) if (!quads || !quads.length)
return 'error:notvisible'; return 'error:notvisible';
@ -536,7 +538,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, point => this._page.touchscreen.tap(point.x, point.y), { ...options, waitAfter: 'disabled' }); return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, point => this._page.touchscreen.tap(point.x, point.y), { ...options, waitAfter: 'disabled' });
} }
async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: { noWaitAfter?: boolean } & types.CommonActionOptions): Promise<string[]> { async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[]> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
const result = await this._selectOption(progress, elements, values, options); const result = await this._selectOption(progress, elements, values, options);
@ -544,7 +546,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: { noWaitAfter?: boolean } & types.CommonActionOptions): Promise<string[] | 'error:notconnected'> { async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[] | 'error:notconnected'> {
let resultingOptions: string[] = []; let resultingOptions: string[] = [];
await this._retryAction(progress, 'select option', async () => { await this._retryAction(progress, 'select option', async () => {
await progress.beforeInputAction(this); await progress.beforeInputAction(this);

View file

@ -155,7 +155,9 @@ export abstract class APIRequestContext extends SdkObject {
} }
const requestUrl = new URL(params.url, defaults.baseURL); const requestUrl = new URL(params.url, defaults.baseURL);
if (params.params) { if (params.encodedParams) {
requestUrl.search = params.encodedParams;
} else if (params.params) {
for (const { name, value } of params.params) for (const { name, value } of params.params)
requestUrl.searchParams.append(name, value); requestUrl.searchParams.append(name, value);
} }

Some files were not shown because too many files have changed in this diff Show more