Merge branch 'main' into sharding-algorithm

This commit is contained in:
Mathias Leppich 2024-09-23 08:41:09 +02:00
commit ab3ab83955
157 changed files with 8118 additions and 1014 deletions

View file

@ -26,7 +26,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
channel: [bidi-chromium, bidi-firefox-beta] channel: [bidi-chromium, bidi-firefox-nightly]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
@ -38,8 +38,8 @@ jobs:
- run: npm run build - run: npm run build
- run: npx playwright install --with-deps chromium - run: npx playwright install --with-deps chromium
if: matrix.channel == 'bidi-chromium' if: matrix.channel == 'bidi-chromium'
- run: npx -y @puppeteer/browsers install firefox@beta - run: npx -y @puppeteer/browsers install firefox@nightly
if: matrix.channel == 'bidi-firefox-beta' if: matrix.channel == 'bidi-firefox-nightly'
- name: Run tests - name: Run tests
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run biditest -- --project=${{ matrix.channel }}* run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run biditest -- --project=${{ matrix.channel }}*
env: 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-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) [![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-130.0.6723.6-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,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows | | | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: | | :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->129.0.6668.42<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Chromium <!-- GEN:chromium-version -->130.0.6723.6<!-- 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 -->130.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: |

View file

@ -1266,6 +1266,99 @@ When set to `minimal`, only record information necessary for routing from HAR. T
Optional setting to control resource content management. If `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is specified, content is stored inline the HAR file. Optional setting to control resource content management. If `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is specified, content is stored inline the HAR file.
## async method: BrowserContext.routeWebSocket
* since: v1.48
This method allows to modify websocket connections that are made by any page in the browser context.
Note that only `WebSocket`s created after this method was called will be routed. It is recommended to call this method before creating any pages.
**Usage**
Below is an example of a simple handler that blocks some websocket messages.
See [WebSocketRoute] for more details and examples.
```js
await context.routeWebSocket('/ws', async ws => {
ws.routeSend(message => {
if (message === 'to-be-blocked')
return;
ws.send(message);
});
await ws.connect();
});
```
```java
context.routeWebSocket("/ws", ws -> {
ws.routeSend(message -> {
if ("to-be-blocked".equals(message))
return;
ws.send(message);
});
ws.connect();
});
```
```python async
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
if message == "to-be-blocked":
return
ws.send(message)
async def handler(ws: WebSocketRoute):
ws.route_send(lambda message: message_handler(ws, message))
await ws.connect()
await context.route_web_socket("/ws", handler)
```
```python sync
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
if message == "to-be-blocked":
return
ws.send(message)
def handler(ws: WebSocketRoute):
ws.route_send(lambda message: message_handler(ws, message))
ws.connect()
context.route_web_socket("/ws", handler)
```
```csharp
await context.RouteWebSocketAsync("/ws", async ws => {
ws.RouteSend(message => {
if (message == "to-be-blocked")
return;
ws.Send(message);
});
await ws.ConnectAsync();
});
```
### param: BrowserContext.routeWebSocket.url
* since: v1.48
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the [`option: baseURL`] from the context options.
### param: BrowserContext.routeWebSocket.handler
* since: v1.48
* langs: js, python
- `handler` <[function]\([WebSocketRoute]\): [Promise<any>|any]>
Handler function to route the WebSocket.
### param: BrowserContext.routeWebSocket.handler
* since: v1.48
* langs: csharp, java
- `handler` <[function]\([WebSocketRoute]\)>
Handler function to route the WebSocket.
## method: BrowserContext.serviceWorkers ## method: BrowserContext.serviceWorkers
* since: v1.11 * since: v1.11
* langs: js, python * langs: js, python

View file

@ -1,30 +1,30 @@
# class: FrameLocator # class: FrameLocator
* since: v1.17 * since: v1.17
FrameLocator represents a view to the `iframe` on the page. It captures the logic sufficient to retrieve the `iframe` and locate elements in that iframe. FrameLocator can be created with either [`method: Page.frameLocator`] or [`method: Locator.frameLocator`] method. FrameLocator represents a view to the `iframe` on the page. It captures the logic sufficient to retrieve the `iframe` and locate elements in that iframe. FrameLocator can be created with either [`method: Locator.contentFrame`], [`method: Page.frameLocator`] or [`method: Locator.frameLocator`] method.
```js ```js
const locator = page.frameLocator('#my-frame').getByText('Submit'); const locator = page.locator('#my-frame').contentFrame().getByText('Submit');
await locator.click(); await locator.click();
``` ```
```java ```java
Locator locator = page.frameLocator("#my-frame").getByText("Submit"); Locator locator = page.locator("#my-frame").contentFrame().getByText("Submit");
locator.click(); locator.click();
``` ```
```python async ```python async
locator = page.frame_locator("#my-frame").get_by_text("Submit") locator = page.locator("#my-frame").content_frame.get_by_text("Submit")
await locator.click() await locator.click()
``` ```
```python sync ```python sync
locator = page.frame_locator("my-frame").get_by_text("Submit") locator = page.locator("my-frame").content_frame.get_by_text("Submit")
locator.click() locator.click()
``` ```
```csharp ```csharp
var locator = page.FrameLocator("#my-frame").GetByText("Submit"); var locator = page.Locator("#my-frame").ContentFrame.GetByText("Submit");
await locator.ClickAsync(); await locator.ClickAsync();
``` ```
@ -34,42 +34,42 @@ Frame locators are strict. This means that all operations on frame locators will
```js ```js
// Throws if there are several frames in DOM: // Throws if there are several frames in DOM:
await page.frameLocator('.result-frame').getByRole('button').click(); await page.locator('.result-frame').contentFrame().getByRole('button').click();
// Works because we explicitly tell locator to pick the first frame: // Works because we explicitly tell locator to pick the first frame:
await page.frameLocator('.result-frame').first().getByRole('button').click(); await page.locator('.result-frame').contentFrame().first().getByRole('button').click();
``` ```
```python async ```python async
# Throws if there are several frames in DOM: # Throws if there are several frames in DOM:
await page.frame_locator('.result-frame').get_by_role('button').click() await page.locator('.result-frame').content_frame.get_by_role('button').click()
# Works because we explicitly tell locator to pick the first frame: # Works because we explicitly tell locator to pick the first frame:
await page.frame_locator('.result-frame').first.get_by_role('button').click() await page.locator('.result-frame').first.content_frame.get_by_role('button').click()
``` ```
```python sync ```python sync
# Throws if there are several frames in DOM: # Throws if there are several frames in DOM:
page.frame_locator('.result-frame').get_by_role('button').click() page.locator('.result-frame').content_frame.get_by_role('button').click()
# Works because we explicitly tell locator to pick the first frame: # Works because we explicitly tell locator to pick the first frame:
page.frame_locator('.result-frame').first.get_by_role('button').click() page.locator('.result-frame').first.content_frame.get_by_role('button').click()
``` ```
```java ```java
// Throws if there are several frames in DOM: // Throws if there are several frames in DOM:
page.frame_locator(".result-frame").getByRole(AriaRole.BUTTON).click(); page.locator(".result-frame").contentFrame().getByRole(AriaRole.BUTTON).click();
// Works because we explicitly tell locator to pick the first frame: // Works because we explicitly tell locator to pick the first frame:
page.frame_locator(".result-frame").first().getByRole(AriaRole.BUTTON).click(); page.locator(".result-frame").first().contentFrame().getByRole(AriaRole.BUTTON).click();
``` ```
```csharp ```csharp
// Throws if there are several frames in DOM: // Throws if there are several frames in DOM:
await page.FrameLocator(".result-frame").GetByRole(AriaRole.Button).ClickAsync(); await page.Locator(".result-frame").ContentFrame.GetByRole(AriaRole.Button).ClickAsync();
// Works because we explicitly tell locator to pick the first frame: // Works because we explicitly tell locator to pick the first frame:
await page.FrameLocator(".result-frame").First.getByRole(AriaRole.Button).ClickAsync(); await page.Locator(".result-frame").First.ContentFrame.getByRole(AriaRole.Button).ClickAsync();
``` ```
**Converting Locator to FrameLocator** **Converting Locator to FrameLocator**
@ -82,6 +82,7 @@ If you have a [FrameLocator] object it can be converted to [Locator] pointing to
## method: FrameLocator.first ## method: FrameLocator.first
* deprecated: Use [`method: Locator.first`] followed by [`method: Locator.contentFrame`] instead.
* since: v1.17 * since: v1.17
- returns: <[FrameLocator]> - returns: <[FrameLocator]>
@ -171,6 +172,7 @@ in that iframe.
### option: FrameLocator.getByTitle.exact = %%-locator-get-by-text-exact-%% ### option: FrameLocator.getByTitle.exact = %%-locator-get-by-text-exact-%%
## method: FrameLocator.last ## method: FrameLocator.last
* deprecated: Use [`method: Locator.last`] followed by [`method: Locator.contentFrame`] instead.
* since: v1.17 * since: v1.17
- returns: <[FrameLocator]> - returns: <[FrameLocator]>
@ -195,6 +197,7 @@ Returns locator to the last matching frame.
* since: v1.33 * since: v1.33
## method: FrameLocator.nth ## method: FrameLocator.nth
* deprecated: Use [`method: Locator.nth`] followed by [`method: Locator.contentFrame`] instead.
* since: v1.17 * since: v1.17
- returns: <[FrameLocator]> - returns: <[FrameLocator]>
@ -217,37 +220,36 @@ For a reverse operation, use [`method: Locator.contentFrame`].
**Usage** **Usage**
```js ```js
const frameLocator = page.frameLocator('iframe[name="embedded"]'); const frameLocator = page.locator('iframe[name="embedded"]').contentFrame();
// ... // ...
const locator = frameLocator.owner(); const locator = frameLocator.owner();
await expect(locator).toBeVisible(); await expect(locator).toBeVisible();
``` ```
```java ```java
FrameLocator frameLocator = page.frameLocator("iframe[name=\"embedded\"]"); FrameLocator frameLocator = page.locator("iframe[name=\"embedded\"]").contentFrame();
// ... // ...
Locator locator = frameLocator.owner(); Locator locator = frameLocator.owner();
assertThat(locator).isVisible(); assertThat(locator).isVisible();
``` ```
```python async ```python async
frame_locator = page.frame_locator("iframe[name=\"embedded\"]") frame_locator = page.locator("iframe[name=\"embedded\"]").content_frame
# ... # ...
locator = frame_locator.owner locator = frame_locator.owner
await expect(locator).to_be_visible() await expect(locator).to_be_visible()
``` ```
```python sync ```python sync
frame_locator = page.frame_locator("iframe[name=\"embedded\"]") frame_locator = page.locator("iframe[name=\"embedded\"]").content_frame
# ... # ...
locator = frame_locator.owner locator = frame_locator.owner
expect(locator).to_be_visible() expect(locator).to_be_visible()
``` ```
```csharp ```csharp
var frameLocator = Page.FrameLocator("iframe[name=\"embedded\"]"); var frameLocator = Page.Locator("iframe[name=\"embedded\"]").ContentFrame;
// ... // ...
var locator = frameLocator.Owner; var locator = frameLocator.Owner;
await Expect(locator).ToBeVisibleAsync(); await Expect(locator).ToBeVisibleAsync();
``` ```

View file

@ -3632,6 +3632,99 @@ When set to `minimal`, only record information necessary for routing from HAR. T
Optional setting to control resource content management. If `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is specified, content is stored inline the HAR file. Optional setting to control resource content management. If `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is specified, content is stored inline the HAR file.
## async method: Page.routeWebSocket
* since: v1.48
This method allows to modify websocket connections that are made by the page.
Note that only `WebSocket`s created after this method was called will be routed. It is recommended to call this method before navigating the page.
**Usage**
Below is an example of a simple handler that blocks some websocket messages.
See [WebSocketRoute] for more details and examples.
```js
await page.routeWebSocket('/ws', async ws => {
ws.routeSend(message => {
if (message === 'to-be-blocked')
return;
ws.send(message);
});
await ws.connect();
});
```
```java
page.routeWebSocket("/ws", ws -> {
ws.routeSend(message -> {
if ("to-be-blocked".equals(message))
return;
ws.send(message);
});
ws.connect();
});
```
```python async
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
if message == "to-be-blocked":
return
ws.send(message)
async def handler(ws: WebSocketRoute):
ws.route_send(lambda message: message_handler(ws, message))
await ws.connect()
await page.route_web_socket("/ws", handler)
```
```python sync
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
if message == "to-be-blocked":
return
ws.send(message)
def handler(ws: WebSocketRoute):
ws.route_send(lambda message: message_handler(ws, message))
ws.connect()
page.route_web_socket("/ws", handler)
```
```csharp
await page.RouteWebSocketAsync("/ws", async ws => {
ws.RouteSend(message => {
if (message == "to-be-blocked")
return;
ws.Send(message);
});
await ws.ConnectAsync();
});
```
### param: Page.routeWebSocket.url
* since: v1.48
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the [`option: baseURL`] from the context options.
### param: Page.routeWebSocket.handler
* since: v1.48
* langs: js, python
- `handler` <[function]\([WebSocketRoute]\): [Promise<any>|any]>
Handler function to route the WebSocket.
### param: Page.routeWebSocket.handler
* since: v1.48
* langs: csharp, java
- `handler` <[function]\([WebSocketRoute]\)>
Handler function to route the WebSocket.
## async method: Page.screenshot ## async method: Page.screenshot
* since: v1.8 * since: v1.8
- returns: <[Buffer]> - returns: <[Buffer]>

View file

@ -0,0 +1,166 @@
# class: WebSocketRoute
* since: v1.48
Whenever a [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) route is set up with [`method: Page.routeWebSocket`] or [`method: BrowserContext.routeWebSocket`], the `WebSocketRoute` object allows to handle the WebSocket.
By default, the routed WebSocket will not actually connect to the server. This way, you can mock entire communcation over the WebSocket. Here is an example that responds to a `"query"` with a `"result"`.
```js
await page.routeWebSocket('/ws', async ws => {
ws.routeSend(message => {
if (message === 'query')
ws.receive('result');
});
});
```
```java
page.routeWebSocket("/ws", ws -> {
ws.routeSend(message -> {
if ("query".equals(message))
ws.receive("result");
});
});
```
```python async
def message_handler(ws, message):
if message == "query":
ws.receive("result")
await page.route_web_socket("/ws", lambda ws: ws.route_send(
lambda message: message_handler(ws, message)
))
```
```python sync
def message_handler(ws, message):
if message == "query":
ws.receive("result")
page.route_web_socket("/ws", lambda ws: ws.route_send(
lambda message: message_handler(ws, message)
))
```
```csharp
await page.RouteWebSocketAsync("/ws", async ws => {
ws.RouteSend(message => {
if (message == "query")
ws.receive("result");
});
});
```
## event: WebSocketRoute.close
* since: v1.48
Emitted when the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) closes.
## async method: WebSocketRoute.close
* since: v1.48
Closes the server connection and the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page.
### option: WebSocketRoute.close.code
* since: v1.48
- `code` <[int]>
Optional [close code](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#code).
### option: WebSocketRoute.close.reason
* since: v1.48
- `reason` <[string]>
Optional [close reason](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#reason).
## async method: WebSocketRoute.connect
* since: v1.48
By default, routed WebSocket does not connect to the server, so you can mock entire WebSocket communication. This method connects to the actual WebSocket server, giving the ability to send and receive messages from the server.
Once connected:
* Messages received from the server will be automatically dispatched to the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page, unless [`method: WebSocketRoute.routeReceive`] is called.
* Messages sent by the `WebSocket.send()` call in the page will be automatically sent to the server, unless [`method: WebSocketRoute.routeSend`] is called.
## method: WebSocketRoute.receive
* since: v1.48
Dispatches a message to the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page, like it was received from the server.
### param: WebSocketRoute.receive.message
* since: v1.48
- `message` <[string]|[Buffer]>
Message to receive.
## async method: WebSocketRoute.routeReceive
* since: v1.48
This method allows to route messages that are received by the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page from the server. This method only makes sense if you are also calling [`method: WebSocketRoute.connect`].
Once this method is called, received messages are not automatically dispatched to the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page - you should do that manually by calling [`method: WebSocketRoute.receive`].
Calling this method again times will override the handler with a new one.
### param: WebSocketRoute.routeReceive.handler
* since: v1.48
* langs: js, python
- `handler` <[function]\([string]\): [Promise<any>|any]>
Handler function to route received messages.
### param: WebSocketRoute.routeReceive.handler
* since: v1.48
* langs: csharp, java
- `handler` <[function]\([WebSocketFrame]\)>
Handler function to route received messages.
## async method: WebSocketRoute.routeSend
* since: v1.48
This method allows to route messages that are sent by `WebSocket.send()` call in the page, instead of actually sending them to the server. Once this method is called, sent messages **are not** automatically forwarded to the server - you should do that manually by calling [`method: WebSocketRoute.send`].
Calling this method again times will override the handler with a new one.
### param: WebSocketRoute.routeSend.handler
* since: v1.48
* langs: js, python
- `handler` <[function]\([string]|[Buffer]\): [Promise<any>|any]>
Handler function to route sent messages.
### param: WebSocketRoute.routeSend.handler
* since: v1.48
* langs: csharp, java
- `handler` <[function]\([WebSocketFrame]\)>
Handler function to route sent messages.
## method: WebSocketRoute.send
* since: v1.48
Sends a message to the server, like it was sent in the page with `WebSocket.send()`.
### param: WebSocketRoute.send.message
* since: v1.48
- `message` <[string]|[Buffer]>
Message to send.
## method: WebSocketRoute.url
* since: v1.48
- returns: <[string]>
URL of the WebSocket created in the page.

379
package-lock.json generated
View file

@ -40,7 +40,7 @@
"@zip.js/zip.js": "^2.7.29", "@zip.js/zip.js": "^2.7.29",
"ansi-styles": "^4.3.0", "ansi-styles": "^4.3.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"chromium-bidi": "^0.6.4", "chromium-bidi": "^0.7.1",
"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",
@ -61,7 +61,7 @@
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"ssim.js": "^3.5.0", "ssim.js": "^3.5.0",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"vite": "^5.0.13", "vite": "^5.4.6",
"ws": "^8.17.1", "ws": "^8.17.1",
"xml2js": "^0.5.0", "xml2js": "^0.5.0",
"yaml": "^2.2.2" "yaml": "^2.2.2"
@ -852,9 +852,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -1517,9 +1517,9 @@
"link": true "link": true
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz",
"integrity": "sha512-jwXtxYbRt1V+CdQSy6Z+uZti7JF5irRKF8hlKfEnF/xJpcNGuuiZMBvuoYM+x9sr9iWGnzrlM0+9hvQ1kgkf1w==", "integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -1529,9 +1529,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz",
"integrity": "sha512-fI9nduZhCccjzlsA/OuAwtFGWocxA4gqXGTLvOyiF8d+8o0fZUeSztixkYjcGq1fGZY3Tkq4yRvHPFxU+jdZ9Q==", "integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1541,9 +1541,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz",
"integrity": "sha512-BcnSPRM76/cD2gQC+rQNGBN6GStBs2pl/FpweW8JYuz5J/IEa0Fr4AtrPv766DB/6b2MZ/AfSIOSGw3nEIP8SA==", "integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1553,9 +1553,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz",
"integrity": "sha512-LDyFB9GRolGN7XI6955aFeI3wCdCUszFWumWU0deHA8VpR3nWRrjG6GtGjBrQxQKFevnUTHKCfPR4IvrW3kCgQ==", "integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1565,9 +1565,21 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz",
"integrity": "sha512-ygrGVhQP47mRh0AAD0zl6QqCbNsf0eTo+vgwkY6LunBcg0f2Jv365GXlDUECIyoXp1kKwL5WW6rsO429DBY/bA==", "integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz",
"integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -1577,9 +1589,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz",
"integrity": "sha512-x+uJ6MAYRlHGe9wi4HQjxpaKHPM3d3JjqqCkeC5gpnnI6OWovLdXTpfa8trjxPLnWKyBsSi5kne+146GAxFt4A==", "integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1589,9 +1601,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz",
"integrity": "sha512-nrRw8ZTQKg6+Lttwqo6a2VxR9tOroa2m91XbdQ2sUUzHoedXlsyvY1fN4xWdqz8PKmf4orDwejxXHjh7YBGUCA==", "integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1601,11 +1613,11 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz",
"integrity": "sha512-xV0d5jDb4aFu84XKr+lcUJ9y3qpIWhttO3Qev97z8DKLXR62LC3cXT/bMZXrjLF9X+P5oSmJTzAhqwUbY96PnA==", "integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==",
"cpu": [ "cpu": [
"ppc64le" "ppc64"
], ],
"optional": true, "optional": true,
"os": [ "os": [
@ -1613,9 +1625,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz",
"integrity": "sha512-SDDhBQwZX6LPRoPYjAZWyL27LbcBo7WdBFWJi5PI9RPCzU8ijzkQn7tt8NXiXRiFMJCVpkuMkBf4OxSxVMizAw==", "integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -1625,9 +1637,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz",
"integrity": "sha512-RxB/qez8zIDshNJDufYlTT0ZTVut5eCpAZ3bdXDU9yTxBzui3KhbGjROK2OYTTor7alM7XBhssgoO3CZ0XD3qA==", "integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -1637,9 +1649,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz",
"integrity": "sha512-C6y6z2eCNCfhZxT9u+jAM2Fup89ZjiG5pIzZIDycs1IwESviLxwkQcFRGLjnDrP+PT+v5i4YFvlcfAs+LnreXg==", "integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1649,9 +1661,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz",
"integrity": "sha512-i0QwbHYfnOMYsBEyjxcwGu5SMIi9sImDVjDg087hpzXqhBSosxkE7gyIYFHgfFl4mr7RrXksIBZ4DoLoP4FhJg==", "integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1661,9 +1673,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz",
"integrity": "sha512-Fq52EYb0riNHLBTAcL0cun+rRwyZ10S9vKzhGKKgeD+XbwunszSY0rVMco5KbOsTlwovP2rTOkiII/fQ4ih/zQ==", "integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1673,9 +1685,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz",
"integrity": "sha512-e/PBHxPdJ00O9p5Ui43+vixSgVf4NlLsmV6QneGERJ3lnjIua/kim6PRFe3iDueT1rQcgSkYP8ZBBXa/h4iPvw==", "integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -1685,9 +1697,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz",
"integrity": "sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag==", "integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2876,9 +2888,9 @@
} }
}, },
"node_modules/chromium-bidi": { "node_modules/chromium-bidi": {
"version": "0.6.5", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.5.tgz", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.7.1.tgz",
"integrity": "sha512-RuLrmzYrxSb0s9SgpB+QN5jJucPduZQ/9SIe76MDxYJuecPW5mxMdacJ1f4EtgiV+R0p3sCkznTMvH0MPGFqjA==", "integrity": "sha512-am9lR+HidiBtPtEYV7aFBpFJaQZhwJbYKr37cOHw0GGR+uiG0O79f20JNLjR0qEwPMuxOHvdBu4HHfimClBOCg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"mitt": "3.0.1", "mitt": "3.0.1",
@ -5862,9 +5874,9 @@
} }
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@ -5917,9 +5929,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.38", "version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -5936,8 +5948,8 @@
], ],
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"picocolors": "^1.0.0", "picocolors": "^1.1.0",
"source-map-js": "^1.2.0" "source-map-js": "^1.2.1"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
@ -6301,9 +6313,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz",
"integrity": "sha512-Qe7w62TyawbDzB4yt32R0+AbIo6m1/sqO7UPzFS8Z/ksL5mrfhA0v4CavfdmFav3D+ub4QeAgsGEe84DoWe/nQ==", "integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==",
"dependencies": { "dependencies": {
"@types/estree": "1.0.5" "@types/estree": "1.0.5"
}, },
@ -6315,21 +6327,22 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.14.0", "@rollup/rollup-android-arm-eabi": "4.21.3",
"@rollup/rollup-android-arm64": "4.14.0", "@rollup/rollup-android-arm64": "4.21.3",
"@rollup/rollup-darwin-arm64": "4.14.0", "@rollup/rollup-darwin-arm64": "4.21.3",
"@rollup/rollup-darwin-x64": "4.14.0", "@rollup/rollup-darwin-x64": "4.21.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.14.0", "@rollup/rollup-linux-arm-gnueabihf": "4.21.3",
"@rollup/rollup-linux-arm64-gnu": "4.14.0", "@rollup/rollup-linux-arm-musleabihf": "4.21.3",
"@rollup/rollup-linux-arm64-musl": "4.14.0", "@rollup/rollup-linux-arm64-gnu": "4.21.3",
"@rollup/rollup-linux-powerpc64le-gnu": "4.14.0", "@rollup/rollup-linux-arm64-musl": "4.21.3",
"@rollup/rollup-linux-riscv64-gnu": "4.14.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.21.3",
"@rollup/rollup-linux-s390x-gnu": "4.14.0", "@rollup/rollup-linux-riscv64-gnu": "4.21.3",
"@rollup/rollup-linux-x64-gnu": "4.14.0", "@rollup/rollup-linux-s390x-gnu": "4.21.3",
"@rollup/rollup-linux-x64-musl": "4.14.0", "@rollup/rollup-linux-x64-gnu": "4.21.3",
"@rollup/rollup-win32-arm64-msvc": "4.14.0", "@rollup/rollup-linux-x64-musl": "4.21.3",
"@rollup/rollup-win32-ia32-msvc": "4.14.0", "@rollup/rollup-win32-arm64-msvc": "4.21.3",
"@rollup/rollup-win32-x64-msvc": "4.14.0", "@rollup/rollup-win32-ia32-msvc": "4.21.3",
"@rollup/rollup-win32-x64-msvc": "4.21.3",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@ -6589,9 +6602,9 @@
} }
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.0", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -7171,13 +7184,13 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.2.8", "version": "5.4.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
"integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==", "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
"dependencies": { "dependencies": {
"esbuild": "^0.20.1", "esbuild": "^0.21.3",
"postcss": "^8.4.38", "postcss": "^8.4.43",
"rollup": "^4.13.0" "rollup": "^4.20.0"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@ -7196,6 +7209,7 @@
"less": "*", "less": "*",
"lightningcss": "^1.21.0", "lightningcss": "^1.21.0",
"sass": "*", "sass": "*",
"sass-embedded": "*",
"stylus": "*", "stylus": "*",
"sugarss": "*", "sugarss": "*",
"terser": "^5.4.0" "terser": "^5.4.0"
@ -7213,6 +7227,9 @@
"sass": { "sass": {
"optional": true "optional": true
}, },
"sass-embedded": {
"optional": true
},
"stylus": { "stylus": {
"optional": true "optional": true
}, },
@ -7243,9 +7260,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/android-arm": { "node_modules/vite/node_modules/@esbuild/android-arm": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -7258,9 +7275,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/android-arm64": { "node_modules/vite/node_modules/@esbuild/android-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7273,9 +7290,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/android-x64": { "node_modules/vite/node_modules/@esbuild/android-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7288,9 +7305,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/darwin-arm64": { "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7303,9 +7320,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/darwin-x64": { "node_modules/vite/node_modules/@esbuild/darwin-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7318,9 +7335,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/freebsd-arm64": { "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7333,9 +7350,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/freebsd-x64": { "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7348,9 +7365,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-arm": { "node_modules/vite/node_modules/@esbuild/linux-arm": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -7363,9 +7380,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-arm64": { "node_modules/vite/node_modules/@esbuild/linux-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7378,9 +7395,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-ia32": { "node_modules/vite/node_modules/@esbuild/linux-ia32": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -7393,9 +7410,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-loong64": { "node_modules/vite/node_modules/@esbuild/linux-loong64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -7408,9 +7425,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-mips64el": { "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -7423,9 +7440,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-ppc64": { "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -7438,9 +7455,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-riscv64": { "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -7453,9 +7470,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-s390x": { "node_modules/vite/node_modules/@esbuild/linux-s390x": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -7468,9 +7485,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-x64": { "node_modules/vite/node_modules/@esbuild/linux-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7483,9 +7500,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/netbsd-x64": { "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7498,9 +7515,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/openbsd-x64": { "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7513,9 +7530,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/sunos-x64": { "node_modules/vite/node_modules/@esbuild/sunos-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7528,9 +7545,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/win32-arm64": { "node_modules/vite/node_modules/@esbuild/win32-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7543,9 +7560,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/win32-ia32": { "node_modules/vite/node_modules/@esbuild/win32-ia32": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -7558,9 +7575,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/win32-x64": { "node_modules/vite/node_modules/@esbuild/win32-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7573,9 +7590,9 @@
} }
}, },
"node_modules/vite/node_modules/esbuild": { "node_modules/vite/node_modules/esbuild": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"hasInstallScript": true, "hasInstallScript": true,
"bin": { "bin": {
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
@ -7584,29 +7601,29 @@
"node": ">=12" "node": ">=12"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.20.2", "@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.20.2", "@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.20.2", "@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.20.2", "@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.20.2", "@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.20.2", "@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.20.2", "@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.20.2", "@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.20.2", "@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.20.2", "@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.20.2", "@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.20.2", "@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.20.2", "@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.20.2", "@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.20.2", "@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.20.2", "@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.20.2", "@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.20.2", "@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.20.2", "@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.20.2", "@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.20.2", "@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.20.2", "@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.20.2" "@esbuild/win32-x64": "0.21.5"
} }
}, },
"node_modules/vitefu": { "node_modules/vitefu": {

View file

@ -79,7 +79,7 @@
"@zip.js/zip.js": "^2.7.29", "@zip.js/zip.js": "^2.7.29",
"ansi-styles": "^4.3.0", "ansi-styles": "^4.3.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"chromium-bidi": "^0.6.4", "chromium-bidi": "^0.7.1",
"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",
@ -100,7 +100,7 @@
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"ssim.js": "^3.5.0", "ssim.js": "^3.5.0",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"vite": "^5.0.13", "vite": "^5.4.6",
"ws": "^8.17.1", "ws": "^8.17.1",
"xml2js": "^0.5.0", "xml2js": "^0.5.0",
"yaml": "^2.2.2" "yaml": "^2.2.2"

View file

@ -3,15 +3,15 @@
"browsers": [ "browsers": [
{ {
"name": "chromium", "name": "chromium",
"revision": "1135", "revision": "1136",
"installByDefault": true, "installByDefault": true,
"browserVersion": "129.0.6668.42" "browserVersion": "130.0.6723.6"
}, },
{ {
"name": "chromium-tip-of-tree", "name": "chromium-tip-of-tree",
"revision": "1259", "revision": "1261",
"installByDefault": false, "installByDefault": false,
"browserVersion": "130.0.6713.0" "browserVersion": "131.0.6726.0"
}, },
{ {
"name": "firefox", "name": "firefox",
@ -27,7 +27,7 @@
}, },
{ {
"name": "webkit", "name": "webkit",
"revision": "2077", "revision": "2080",
"installByDefault": true, "installByDefault": true,
"revisionOverrides": { "revisionOverrides": {
"mac10.14": "1446", "mac10.14": "1446",

View file

@ -397,7 +397,7 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
process.stdout.write('\n-------------8<-------------\n'); process.stdout.write('\n-------------8<-------------\n');
const autoExitCondition = process.env.PWTEST_CLI_AUTO_EXIT_WHEN; const autoExitCondition = process.env.PWTEST_CLI_AUTO_EXIT_WHEN;
if (autoExitCondition && text.includes(autoExitCondition)) if (autoExitCondition && text.includes(autoExitCondition))
Promise.all(context.pages().map(async p => p.close())); closeBrowser();
}; };
// Make sure we exit abnormally when browser crashes. // Make sure we exit abnormally when browser crashes.
const logs: string[] = []; const logs: string[] = [];
@ -504,7 +504,7 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
if (hasPage) if (hasPage)
return; return;
// Avoid the error when the last page is closed because the browser has been closed. // Avoid the error when the last page is closed because the browser has been closed.
closeBrowser().catch(e => null); closeBrowser().catch(() => {});
}); });
}); });
process.on('SIGINT', async () => { process.on('SIGINT', async () => {
@ -560,17 +560,13 @@ 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 tracesDir = path.join(os.tmpdir(), `recorder-trace-${Date.now()}`); const tracesDir = path.join(os.tmpdir(), `playwright-recorder-trace-${Date.now()}`);
const { context, launchOptions, contextOptions } = await launchContext(options, { const { context, launchOptions, contextOptions } = await launchContext(options, {
headless: !!process.env.PWTEST_CLI_HEADLESS, headless: !!process.env.PWTEST_CLI_HEADLESS,
executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH, executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH,
tracesDir, tracesDir,
}); });
dotenv.config({ path: 'playwright.env' }); 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,
@ -578,6 +574,7 @@ async function codegen(options: Options & { target: string, output?: string, tes
device: options.device, device: options.device,
saveStorage: options.saveStorage, saveStorage: options.saveStorage,
mode: 'recording', mode: 'recording',
codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions',
testIdAttributeName, testIdAttributeName,
outputFile: outputFile ? path.resolve(outputFile) : undefined, outputFile: outputFile ? path.resolve(outputFile) : undefined,
}); });

View file

@ -34,7 +34,7 @@ export { TimeoutError } from './errors';
export { Frame } from './frame'; export { Frame } from './frame';
export { Keyboard, Mouse, Touchscreen } from './input'; export { Keyboard, Mouse, Touchscreen } from './input';
export { JSHandle } from './jsHandle'; export { JSHandle } from './jsHandle';
export { Request, Response, Route, WebSocket } from './network'; export { Request, Response, Route, WebSocket, WebSocketRoute } from './network';
export { APIRequest, APIRequestContext, APIResponse } from './fetch'; export { APIRequest, APIRequestContext, APIResponse } from './fetch';
export { Page } from './page'; export { Page } from './page';
export { Selectors } from './selectors'; export { Selectors } from './selectors';

View file

@ -48,6 +48,7 @@ import { Clock } from './clock';
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext { export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
_pages = new Set<Page>(); _pages = new Set<Page>();
_routes: network.RouteHandler[] = []; _routes: network.RouteHandler[] = [];
_webSocketRoutes: network.WebSocketRouteHandler[] = [];
readonly _browser: Browser | null = null; readonly _browser: Browser | null = null;
_browserType: BrowserType | undefined; _browserType: BrowserType | undefined;
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>(); readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
@ -90,6 +91,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
this._channel.on('close', () => this._onClose()); this._channel.on('close', () => this._onClose());
this._channel.on('page', ({ page }) => this._onPage(Page.from(page))); this._channel.on('page', ({ page }) => this._onPage(Page.from(page)));
this._channel.on('route', ({ route }) => this._onRoute(network.Route.from(route))); this._channel.on('route', ({ route }) => this._onRoute(network.Route.from(route)));
this._channel.on('webSocketRoute', ({ webSocketRoute }) => this._onWebSocketRoute(network.WebSocketRoute.from(webSocketRoute)));
this._channel.on('backgroundPage', ({ page }) => { this._channel.on('backgroundPage', ({ page }) => {
const backgroundPage = Page.from(page); const backgroundPage = Page.from(page);
this._backgroundPages.add(backgroundPage); this._backgroundPages.add(backgroundPage);
@ -218,7 +220,15 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
} }
// If the page is closed or unrouteAll() was called without waiting and interception disabled, // If the page is closed or unrouteAll() was called without waiting and interception disabled,
// the method will throw an error - silence it. // the method will throw an error - silence it.
await route._innerContinue(true).catch(() => {}); await route._innerContinue(true /* isFallback */).catch(() => {});
}
async _onWebSocketRoute(webSocketRoute: network.WebSocketRoute) {
const routeHandler = this._webSocketRoutes.find(route => route.matches(webSocketRoute.url()));
if (routeHandler)
await routeHandler.handle(webSocketRoute);
else
await webSocketRoute.connect();
} }
async _onBinding(bindingCall: BindingCall) { async _onBinding(bindingCall: BindingCall) {
@ -328,6 +338,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await this._updateInterceptionPatterns(); await this._updateInterceptionPatterns();
} }
async routeWebSocket(url: URLMatch, handler: network.WebSocketRouteHandlerCallback): Promise<void> {
this._webSocketRoutes.unshift(new network.WebSocketRouteHandler(this._options.baseURL, url, handler));
await this._updateWebSocketInterceptionPatterns();
}
async _recordIntoHAR(har: string, page: Page | null, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full'} = {}): Promise<void> { async _recordIntoHAR(har: string, page: Page | null, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full'} = {}): Promise<void> {
const { harId } = await this._channel.harStart({ const { harId } = await this._channel.harStart({
page: page?._channel, page: page?._channel,
@ -387,6 +402,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await this._channel.setNetworkInterceptionPatterns({ patterns }); await this._channel.setNetworkInterceptionPatterns({ patterns });
} }
private async _updateWebSocketInterceptionPatterns() {
const patterns = network.WebSocketRouteHandler.prepareInterceptionPatterns(this._webSocketRoutes);
await this._channel.setWebSocketInterceptionPatterns({ patterns });
}
_effectiveCloseReason(): string | undefined { _effectiveCloseReason(): string | undefined {
return this._closeReason || this._browser?._closeReason; return this._closeReason || this._browser?._closeReason;
} }
@ -472,17 +492,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await this._closedPromise; await this._closedPromise;
} }
async _enableRecorder(params: { async _enableRecorder(params: channels.BrowserContextEnableRecorderParams) {
language: string, await this._channel.enableRecorder(params);
launchOptions?: LaunchOptions,
contextOptions?: BrowserContextOptions,
device?: string,
saveStorage?: string,
mode?: 'recording' | 'inspecting',
testIdAttributeName?: string,
outputFile?: string,
}) {
await this._channel.recorderSupplementEnable(params);
} }
} }

View file

@ -40,6 +40,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
_logger: Logger | undefined; _logger: Logger | undefined;
readonly _instrumentation: ClientInstrumentation; readonly _instrumentation: ClientInstrumentation;
private _eventToSubscriptionMapping: Map<string, string> = new Map(); private _eventToSubscriptionMapping: Map<string, string> = new Map();
private _isInternalType = false;
_wasCollected: boolean = false; _wasCollected: boolean = false;
constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits<T>) { constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits<T>) {
@ -61,6 +62,10 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
this._initializer = initializer; this._initializer = initializer;
} }
protected markAsInternalType() {
this._isInternalType = true;
}
_setEventToSubscriptionMapping(mapping: Map<string, string>) { _setEventToSubscriptionMapping(mapping: Map<string, string>) {
this._eventToSubscriptionMapping = mapping; this._eventToSubscriptionMapping = mapping;
} }
@ -173,7 +178,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
let apiName: string | undefined = stackTrace.apiName; let apiName: string | undefined = stackTrace.apiName;
const frames: channels.StackFrame[] = stackTrace.frames; const frames: channels.StackFrame[] = stackTrace.frames;
isInternal = isInternal || this._type === 'LocalUtils'; isInternal = isInternal || this._isInternalType;
if (isInternal) if (isInternal)
apiName = undefined; apiName = undefined;

View file

@ -21,7 +21,7 @@ import { ChannelOwner } from './channelOwner';
import { ElementHandle } from './elementHandle'; import { ElementHandle } from './elementHandle';
import { Frame } from './frame'; import { Frame } from './frame';
import { JSHandle } from './jsHandle'; import { JSHandle } from './jsHandle';
import { Request, Response, Route, WebSocket } from './network'; import { Request, Response, Route, WebSocket, WebSocketRoute } from './network';
import { Page, BindingCall } from './page'; import { Page, BindingCall } from './page';
import { Worker } from './worker'; import { Worker } from './worker';
import { Dialog } from './dialog'; import { Dialog } from './dialog';
@ -309,6 +309,9 @@ export class Connection extends EventEmitter {
case 'WebSocket': case 'WebSocket':
result = new WebSocket(parent, type, guid, initializer); result = new WebSocket(parent, type, guid, initializer);
break; break;
case 'WebSocketRoute':
result = new WebSocketRoute(parent, type, guid, initializer);
break;
case 'Worker': case 'Worker':
result = new Worker(parent, type, guid, initializer); result = new Worker(parent, type, guid, initializer);
break; break;

View file

@ -85,6 +85,10 @@ export const Events = {
FrameSent: 'framesent', FrameSent: 'framesent',
}, },
WebSocketRoute: {
Close: 'close',
},
Worker: { Worker: {
Close: 'close', Close: 'close',
}, },

View file

@ -33,6 +33,7 @@ export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
this.markAsInternalType();
this.devices = {}; this.devices = {};
for (const { name, descriptor } of initializer.deviceDescriptors) for (const { name, descriptor } of initializer.deviceDescriptors)
this.devices[name] = descriptor; this.devices[name] = descriptor;

View file

@ -299,6 +299,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.RouteInitializer) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.RouteInitializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
this.markAsInternalType();
} }
request(): Request { request(): Request {
@ -325,7 +326,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
async abort(errorCode?: string) { async abort(errorCode?: string) {
await this._handleRoute(async () => { await this._handleRoute(async () => {
await this._raceWithTargetClose(this._channel.abort({ requestUrl: this.request()._initializer.url, errorCode })); await this._raceWithTargetClose(this._channel.abort({ errorCode }));
}); });
} }
@ -409,7 +410,6 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
headers['content-length'] = String(length); headers['content-length'] = String(length);
await this._raceWithTargetClose(this._channel.fulfill({ await this._raceWithTargetClose(this._channel.fulfill({
requestUrl: this.request()._initializer.url,
status: statusOption || 200, status: statusOption || 200,
headers: headersObjectToArray(headers), headers: headersObjectToArray(headers),
body, body,
@ -421,7 +421,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
async continue(options: FallbackOverrides = {}) { async continue(options: FallbackOverrides = {}) {
await this._handleRoute(async () => { await this._handleRoute(async () => {
this.request()._applyFallbackOverrides(options); this.request()._applyFallbackOverrides(options);
await this._innerContinue(); await this._innerContinue(false /* isFallback */);
}); });
} }
@ -436,22 +436,143 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
chain.resolve(done); chain.resolve(done);
} }
async _innerContinue(internal = false) { async _innerContinue(isFallback: boolean) {
const options = this.request()._fallbackOverridesForContinue(); const options = this.request()._fallbackOverridesForContinue();
return await this._wrapApiCall(async () => { return await this._raceWithTargetClose(this._channel.continue({
await this._raceWithTargetClose(this._channel.continue({ url: options.url,
requestUrl: this.request()._initializer.url, method: options.method,
url: options.url, headers: options.headers ? headersObjectToArray(options.headers) : undefined,
method: options.method, postData: options.postDataBuffer,
headers: options.headers ? headersObjectToArray(options.headers) : undefined, isFallback,
postData: options.postDataBuffer, }));
isFallback: internal, }
})); }
}, !!internal);
export class WebSocketRoute extends ChannelOwner<channels.WebSocketRouteChannel> implements api.WebSocketRoute {
static from(route: channels.WebSocketRouteChannel): WebSocketRoute {
return (route as any)._object;
}
private _routeSendHandler?: (message: string | Buffer) => any;
private _routeReceiveHandler?: (message: string | Buffer) => any;
private _connected = false;
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.WebSocketRouteInitializer) {
super(parent, type, guid, initializer);
this._channel.on('messageFromPage', ({ message, isBase64 }) => {
if (this._routeSendHandler)
this._routeSendHandler(isBase64 ? Buffer.from(message, 'base64') : message);
else
this._channel.sendToServer({ message, isBase64 }).catch(() => {});
});
this._channel.on('messageFromServer', ({ message, isBase64 }) => {
if (this._routeReceiveHandler)
this._routeReceiveHandler(isBase64 ? Buffer.from(message, 'base64') : message);
else
this._channel.sendToPage({ message, isBase64 }).catch(() => {});
});
this._channel.on('close', () => this.emit(Events.WebSocketRoute.Close));
}
url() {
return this._initializer.url;
}
async close(options: { code?: number, reason?: string } = {}) {
try {
await this._channel.close(options);
} catch (e) {
if (isTargetClosedError(e))
return;
throw e;
}
}
async connect() {
this._connected = true;
await this._channel.connect();
}
send(message: string | Buffer) {
if (isString(message))
this._channel.sendToServer({ message, isBase64: false }).catch(() => {});
else
this._channel.sendToServer({ message: message.toString('base64'), isBase64: true }).catch(() => {});
}
receive(message: string | Buffer) {
if (isString(message))
this._channel.sendToPage({ message, isBase64: false }).catch(() => {});
else
this._channel.sendToPage({ message: message.toString('base64'), isBase64: true }).catch(() => {});
}
routeSend(handler: (message: string | Buffer) => any) {
this._routeSendHandler = handler;
}
routeReceive(handler: (message: string | Buffer) => any) {
this._routeReceiveHandler = handler;
}
async [Symbol.asyncDispose]() {
await this.close();
}
async _afterHandle() {
if (this._connected)
return;
if (this._routeReceiveHandler)
throw new Error(`WebSocketRoute.routeReceive() call had no effect. Make sure to call WebSocketRoute.connect() as well.`);
// Ensure that websocket is "open", so that test can send messages to it
// without an actual server connection.
await this._channel.ensureOpened();
}
}
export class WebSocketRouteHandler {
private readonly _baseURL: string | undefined;
readonly url: URLMatch;
readonly handler: WebSocketRouteHandlerCallback;
constructor(baseURL: string | undefined, url: URLMatch, handler: WebSocketRouteHandlerCallback) {
this._baseURL = baseURL;
this.url = url;
this.handler = handler;
}
static prepareInterceptionPatterns(handlers: WebSocketRouteHandler[]) {
const patterns: channels.BrowserContextSetWebSocketInterceptionPatternsParams['patterns'] = [];
let all = false;
for (const handler of handlers) {
if (isString(handler.url))
patterns.push({ glob: handler.url });
else if (isRegExp(handler.url))
patterns.push({ regexSource: handler.url.source, regexFlags: handler.url.flags });
else
all = true;
}
if (all)
return [{ glob: '**/*' }];
return patterns;
}
public matches(wsURL: string): boolean {
return urlMatches(this._baseURL, wsURL, this.url);
}
public async handle(webSocketRoute: WebSocketRoute) {
const handler = this.handler;
await handler(webSocketRoute);
await webSocketRoute._afterHandle();
} }
} }
export type RouteHandlerCallback = (route: Route, request: Request) => Promise<any> | void; export type RouteHandlerCallback = (route: Route, request: Request) => Promise<any> | void;
export type WebSocketRouteHandlerCallback = (ws: WebSocketRoute) => Promise<any> | void;
export type ResourceTiming = { export type ResourceTiming = {
startTime: number; startTime: number;

View file

@ -40,7 +40,7 @@ import { Keyboard, Mouse, Touchscreen } from './input';
import { assertMaxArguments, JSHandle, parseResult, serializeArgument } from './jsHandle'; import { assertMaxArguments, JSHandle, parseResult, serializeArgument } from './jsHandle';
import type { FrameLocator, Locator, LocatorOptions } from './locator'; import type { FrameLocator, Locator, LocatorOptions } from './locator';
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils'; import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
import { type RouteHandlerCallback, type Request, Response, Route, RouteHandler, validateHeaders, WebSocket } from './network'; import { type RouteHandlerCallback, type Request, Response, Route, RouteHandler, validateHeaders, WebSocket, type WebSocketRouteHandlerCallback, WebSocketRoute, WebSocketRouteHandler } from './network';
import type { FilePayload, Headers, LifecycleEvent, SelectOption, SelectOptionOptions, Size, WaitForEventOptions, WaitForFunctionOptions } from './types'; import type { FilePayload, Headers, LifecycleEvent, SelectOption, SelectOptionOptions, Size, WaitForEventOptions, WaitForFunctionOptions } from './types';
import { Video } from './video'; import { Video } from './video';
import { Waiter } from './waiter'; import { Waiter } from './waiter';
@ -78,6 +78,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
readonly _closedOrCrashedScope = new LongStandingScope(); readonly _closedOrCrashedScope = new LongStandingScope();
private _viewportSize: Size | null; private _viewportSize: Size | null;
_routes: RouteHandler[] = []; _routes: RouteHandler[] = [];
_webSocketRoutes: WebSocketRouteHandler[] = [];
readonly accessibility: Accessibility; readonly accessibility: Accessibility;
readonly coverage: Coverage; readonly coverage: Coverage;
@ -137,6 +138,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
this._channel.on('frameDetached', ({ frame }) => this._onFrameDetached(Frame.from(frame))); this._channel.on('frameDetached', ({ frame }) => this._onFrameDetached(Frame.from(frame)));
this._channel.on('locatorHandlerTriggered', ({ uid }) => this._onLocatorHandlerTriggered(uid)); this._channel.on('locatorHandlerTriggered', ({ uid }) => this._onLocatorHandlerTriggered(uid));
this._channel.on('route', ({ route }) => this._onRoute(Route.from(route))); this._channel.on('route', ({ route }) => this._onRoute(Route.from(route)));
this._channel.on('webSocketRoute', ({ webSocketRoute }) => this._onWebSocketRoute(WebSocketRoute.from(webSocketRoute)));
this._channel.on('video', ({ artifact }) => { this._channel.on('video', ({ artifact }) => {
const artifactObject = Artifact.from(artifact); const artifactObject = Artifact.from(artifact);
this._forceVideo()._artifactReady(artifactObject); this._forceVideo()._artifactReady(artifactObject);
@ -200,6 +202,14 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
await this._browserContext._onRoute(route); await this._browserContext._onRoute(route);
} }
private async _onWebSocketRoute(webSocketRoute: WebSocketRoute) {
const routeHandler = this._webSocketRoutes.find(route => route.matches(webSocketRoute.url()));
if (routeHandler)
await routeHandler.handle(webSocketRoute);
else
await this._browserContext._onWebSocketRoute(webSocketRoute);
}
async _onBinding(bindingCall: BindingCall) { async _onBinding(bindingCall: BindingCall) {
const func = this._bindings.get(bindingCall._initializer.name); const func = this._bindings.get(bindingCall._initializer.name);
if (func) { if (func) {
@ -515,6 +525,11 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
await harRouter.addPageRoute(this); await harRouter.addPageRoute(this);
} }
async routeWebSocket(url: URLMatch, handler: WebSocketRouteHandlerCallback): Promise<void> {
this._webSocketRoutes.unshift(new WebSocketRouteHandler(this._browserContext._options.baseURL, url, handler));
await this._updateWebSocketInterceptionPatterns();
}
private _disposeHarRouters() { private _disposeHarRouters() {
this._harRouters.forEach(router => router.dispose()); this._harRouters.forEach(router => router.dispose());
this._harRouters = []; this._harRouters = [];
@ -551,6 +566,11 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
await this._channel.setNetworkInterceptionPatterns({ patterns }); await this._channel.setNetworkInterceptionPatterns({ patterns });
} }
private async _updateWebSocketInterceptionPatterns() {
const patterns = WebSocketRouteHandler.prepareInterceptionPatterns(this._webSocketRoutes);
await this._channel.setWebSocketInterceptionPatterns({ patterns });
}
async screenshot(options: Omit<channels.PageScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> { async screenshot(options: Omit<channels.PageScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
const copy: channels.PageScreenshotOptions = { ...options, mask: undefined }; const copy: channels.PageScreenshotOptions = { ...options, mask: undefined };
if (!copy.type) if (!copy.type)

View file

@ -31,20 +31,18 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.TracingInitializer) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.TracingInitializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
this.markAsInternalType();
} }
async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, _live?: boolean } = {}) { async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, _live?: boolean } = {}) {
this._includeSources = !!options.sources; this._includeSources = !!options.sources;
const traceName = await this._wrapApiCall(async () => { await this._channel.tracingStart({
await this._channel.tracingStart({ name: options.name,
name: options.name, snapshots: options.snapshots,
snapshots: options.snapshots, screenshots: options.screenshots,
screenshots: options.screenshots, live: options._live,
live: options._live, });
}); const { traceName } = await this._channel.tracingStartChunk({ name: options.name, title: options.title });
const response = await this._channel.tracingStartChunk({ name: options.name, title: options.title });
return response.traceName;
}, true);
await this._startCollectingStacks(traceName); await this._startCollectingStacks(traceName);
} }
@ -63,16 +61,12 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
} }
async stopChunk(options: { path?: string } = {}) { async stopChunk(options: { path?: string } = {}) {
await this._wrapApiCall(async () => { await this._doStopChunk(options.path);
await this._doStopChunk(options.path);
}, true);
} }
async stop(options: { path?: string } = {}) { async stop(options: { path?: string } = {}) {
await this._wrapApiCall(async () => { await this._doStopChunk(options.path);
await this._doStopChunk(options.path); await this._channel.tracingStop();
await this._channel.tracingStop();
}, true);
} }
private async _doStopChunk(filePath: string | undefined) { private async _doStopChunk(filePath: string | undefined) {

View file

@ -832,6 +832,9 @@ scheme.BrowserContextPageErrorEvent = tObject({
scheme.BrowserContextRouteEvent = tObject({ scheme.BrowserContextRouteEvent = tObject({
route: tChannel(['Route']), route: tChannel(['Route']),
}); });
scheme.BrowserContextWebSocketRouteEvent = tObject({
webSocketRoute: tChannel(['WebSocketRoute']),
});
scheme.BrowserContextVideoEvent = tObject({ scheme.BrowserContextVideoEvent = tObject({
artifact: tChannel(['Artifact']), artifact: tChannel(['Artifact']),
}); });
@ -943,6 +946,14 @@ scheme.BrowserContextSetNetworkInterceptionPatternsParams = tObject({
})), })),
}); });
scheme.BrowserContextSetNetworkInterceptionPatternsResult = tOptional(tObject({})); scheme.BrowserContextSetNetworkInterceptionPatternsResult = tOptional(tObject({}));
scheme.BrowserContextSetWebSocketInterceptionPatternsParams = tObject({
patterns: tArray(tObject({
glob: tOptional(tString),
regexSource: tOptional(tString),
regexFlags: tOptional(tString),
})),
});
scheme.BrowserContextSetWebSocketInterceptionPatternsResult = tOptional(tObject({}));
scheme.BrowserContextSetOfflineParams = tObject({ scheme.BrowserContextSetOfflineParams = tObject({
offline: tBoolean, offline: tBoolean,
}); });
@ -954,9 +965,10 @@ scheme.BrowserContextStorageStateResult = tObject({
}); });
scheme.BrowserContextPauseParams = tOptional(tObject({})); scheme.BrowserContextPauseParams = tOptional(tObject({}));
scheme.BrowserContextPauseResult = tOptional(tObject({})); scheme.BrowserContextPauseResult = tOptional(tObject({}));
scheme.BrowserContextRecorderSupplementEnableParams = tObject({ scheme.BrowserContextEnableRecorderParams = tObject({
language: tOptional(tString), language: tOptional(tString),
mode: tOptional(tEnum(['inspecting', 'recording'])), mode: tOptional(tEnum(['inspecting', 'recording'])),
codegenMode: tOptional(tEnum(['actions', 'trace-events'])),
pauseOnNextStatement: tOptional(tBoolean), pauseOnNextStatement: tOptional(tBoolean),
testIdAttributeName: tOptional(tString), testIdAttributeName: tOptional(tString),
launchOptions: tOptional(tAny), launchOptions: tOptional(tAny),
@ -966,7 +978,7 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({
outputFile: tOptional(tString), outputFile: tOptional(tString),
omitCallTracking: tOptional(tBoolean), omitCallTracking: tOptional(tBoolean),
}); });
scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({})); scheme.BrowserContextEnableRecorderResult = tOptional(tObject({}));
scheme.BrowserContextNewCDPSessionParams = tObject({ scheme.BrowserContextNewCDPSessionParams = tObject({
page: tOptional(tChannel(['Page'])), page: tOptional(tChannel(['Page'])),
frame: tOptional(tChannel(['Frame'])), frame: tOptional(tChannel(['Frame'])),
@ -1070,6 +1082,9 @@ scheme.PageLocatorHandlerTriggeredEvent = tObject({
scheme.PageRouteEvent = tObject({ scheme.PageRouteEvent = tObject({
route: tChannel(['Route']), route: tChannel(['Route']),
}); });
scheme.PageWebSocketRouteEvent = tObject({
webSocketRoute: tChannel(['WebSocketRoute']),
});
scheme.PageVideoEvent = tObject({ scheme.PageVideoEvent = tObject({
artifact: tChannel(['Artifact']), artifact: tChannel(['Artifact']),
}); });
@ -1211,6 +1226,14 @@ scheme.PageSetNetworkInterceptionPatternsParams = tObject({
})), })),
}); });
scheme.PageSetNetworkInterceptionPatternsResult = tOptional(tObject({})); scheme.PageSetNetworkInterceptionPatternsResult = tOptional(tObject({}));
scheme.PageSetWebSocketInterceptionPatternsParams = tObject({
patterns: tArray(tObject({
glob: tOptional(tString),
regexSource: tOptional(tString),
regexFlags: tOptional(tString),
})),
});
scheme.PageSetWebSocketInterceptionPatternsResult = tOptional(tObject({}));
scheme.PageSetViewportSizeParams = tObject({ scheme.PageSetViewportSizeParams = tObject({
viewportSize: tObject({ viewportSize: tObject({
width: tNumber, width: tNumber,
@ -2093,7 +2116,6 @@ scheme.RouteRedirectNavigationRequestParams = tObject({
scheme.RouteRedirectNavigationRequestResult = tOptional(tObject({})); scheme.RouteRedirectNavigationRequestResult = tOptional(tObject({}));
scheme.RouteAbortParams = tObject({ scheme.RouteAbortParams = tObject({
errorCode: tOptional(tString), errorCode: tOptional(tString),
requestUrl: tString,
}); });
scheme.RouteAbortResult = tOptional(tObject({})); scheme.RouteAbortResult = tOptional(tObject({}));
scheme.RouteContinueParams = tObject({ scheme.RouteContinueParams = tObject({
@ -2101,7 +2123,6 @@ scheme.RouteContinueParams = tObject({
method: tOptional(tString), method: tOptional(tString),
headers: tOptional(tArray(tType('NameValue'))), headers: tOptional(tArray(tType('NameValue'))),
postData: tOptional(tBinary), postData: tOptional(tBinary),
requestUrl: tString,
isFallback: tBoolean, isFallback: tBoolean,
}); });
scheme.RouteContinueResult = tOptional(tObject({})); scheme.RouteContinueResult = tOptional(tObject({}));
@ -2111,9 +2132,39 @@ scheme.RouteFulfillParams = tObject({
body: tOptional(tString), body: tOptional(tString),
isBase64: tOptional(tBoolean), isBase64: tOptional(tBoolean),
fetchResponseUid: tOptional(tString), fetchResponseUid: tOptional(tString),
requestUrl: tString,
}); });
scheme.RouteFulfillResult = tOptional(tObject({})); scheme.RouteFulfillResult = tOptional(tObject({}));
scheme.WebSocketRouteInitializer = tObject({
url: tString,
});
scheme.WebSocketRouteMessageFromPageEvent = tObject({
message: tString,
isBase64: tBoolean,
});
scheme.WebSocketRouteMessageFromServerEvent = tObject({
message: tString,
isBase64: tBoolean,
});
scheme.WebSocketRouteCloseEvent = tOptional(tObject({}));
scheme.WebSocketRouteConnectParams = tOptional(tObject({}));
scheme.WebSocketRouteConnectResult = tOptional(tObject({}));
scheme.WebSocketRouteEnsureOpenedParams = tOptional(tObject({}));
scheme.WebSocketRouteEnsureOpenedResult = tOptional(tObject({}));
scheme.WebSocketRouteSendToPageParams = tObject({
message: tString,
isBase64: tBoolean,
});
scheme.WebSocketRouteSendToPageResult = tOptional(tObject({}));
scheme.WebSocketRouteSendToServerParams = tObject({
message: tString,
isBase64: tBoolean,
});
scheme.WebSocketRouteSendToServerResult = tOptional(tObject({}));
scheme.WebSocketRouteCloseParams = tObject({
code: tOptional(tNumber),
reason: tOptional(tString),
});
scheme.WebSocketRouteCloseResult = tOptional(tObject({}));
scheme.ResourceTiming = tObject({ scheme.ResourceTiming = tObject({
startTime: tNumber, startTime: tNumber,
domainLookupStart: tNumber, domainLookupStart: tNumber,

View file

@ -72,7 +72,7 @@ export class BidiConnection {
let context; let context;
if ('context' in object.params) if ('context' in object.params)
context = object.params.context; context = object.params.context;
else if (object.method === 'log.entryAdded') else if (object.method === 'log.entryAdded' || object.method === 'script.message')
context = object.params.source?.context; context = object.params.source?.context;
if (context) { if (context) {
const session = this._browsingContextToSession.get(context); const session = this._browsingContextToSession.get(context);

View file

@ -23,7 +23,7 @@ import { BidiSerializer } from './third_party/bidiSerializer';
export class BidiExecutionContext implements js.ExecutionContextDelegate { export class BidiExecutionContext implements js.ExecutionContextDelegate {
private readonly _session: BidiSession; private readonly _session: BidiSession;
private readonly _target: bidi.Script.Target; readonly _target: bidi.Script.Target;
constructor(session: BidiSession, realmInfo: bidi.Script.RealmInfo) { constructor(session: BidiSession, realmInfo: bidi.Script.RealmInfo) {
this._session = session; this._session = session;

View file

@ -51,6 +51,14 @@ export class BidiFirefox extends BrowserType {
override amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env { override amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
if (!path.isAbsolute(os.homedir())) 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?`); throw new Error(`Cannot launch Firefox with relative home directory. Did you set ${os.platform() === 'win32' ? 'USERPROFILE' : 'HOME'} to a relative path?`);
env = {
...env,
'MOZ_CRASHREPORTER': '1',
'MOZ_CRASHREPORTER_NO_REPORT': '1',
'MOZ_CRASHREPORTER_SHUTDOWN': '1',
};
if (os.platform() === 'linux') { if (os.platform() === 'linux') {
// Always remove SNAP_NAME and SNAP_INSTANCE_NAME env variables since they // Always remove SNAP_NAME and SNAP_INSTANCE_NAME env variables since they
// confuse Firefox: in our case, builds never come from SNAP. // confuse Firefox: in our case, builds never come from SNAP.

View file

@ -21,7 +21,8 @@ import type * as accessibility from '../accessibility';
import * as dom from '../dom'; import * as dom from '../dom';
import * as dialog from '../dialog'; import * as dialog from '../dialog';
import type * as frames from '../frames'; import type * as frames from '../frames';
import { type InitScript, Page, type PageDelegate } from '../page'; import { Page } from '../page';
import type { InitScript, PageDelegate } from '../page';
import type { Progress } from '../progress'; import type { Progress } from '../progress';
import type * as types from '../types'; import type * as types from '../types';
import type { BidiBrowserContext } from './bidiBrowser'; import type { BidiBrowserContext } from './bidiBrowser';
@ -33,6 +34,7 @@ import { BidiNetworkManager } from './bidiNetworkManager';
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
const kPlaywrightBindingChannel = 'playwrightChannel';
export class BidiPage implements PageDelegate { export class BidiPage implements PageDelegate {
readonly rawMouse: RawMouseImpl; readonly rawMouse: RawMouseImpl;
@ -62,6 +64,7 @@ export class BidiPage implements PageDelegate {
this._page.on(Page.Events.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false)); this._page.on(Page.Events.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false));
this._sessionListeners = [ this._sessionListeners = [
eventsHelper.addEventListener(bidiSession, 'script.realmCreated', this._onRealmCreated.bind(this)), eventsHelper.addEventListener(bidiSession, 'script.realmCreated', this._onRealmCreated.bind(this)),
eventsHelper.addEventListener(bidiSession, 'script.message', this._onScriptMessage.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.contextDestroyed', this._onBrowsingContextDestroyed.bind(this)), eventsHelper.addEventListener(bidiSession, 'browsingContext.contextDestroyed', this._onBrowsingContextDestroyed.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationStarted', this._onNavigationStarted.bind(this)), eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationStarted', this._onNavigationStarted.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationAborted', this._onNavigationAborted.bind(this)), eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationAborted', this._onNavigationAborted.bind(this)),
@ -93,6 +96,7 @@ export class BidiPage implements PageDelegate {
this.updateHttpCredentials(), this.updateHttpCredentials(),
this.updateRequestInterception(), this.updateRequestInterception(),
this._updateViewport(), this._updateViewport(),
this._installMainBinding(),
this._addAllInitScripts(), this._addAllInitScripts(),
]); ]);
} }
@ -315,18 +319,63 @@ export class BidiPage implements PageDelegate {
}); });
} }
goBack(): Promise<boolean> { async goBack(): Promise<boolean> {
throw new Error('Method not implemented.'); return await this._session.send('browsingContext.traverseHistory', {
context: this._session.sessionId,
delta: -1,
}).then(() => true).catch(() => false);
} }
goForward(): Promise<boolean> { async goForward(): Promise<boolean> {
throw new Error('Method not implemented.'); return await this._session.send('browsingContext.traverseHistory', {
context: this._session.sessionId,
delta: +1,
}).then(() => true).catch(() => false);
} }
async forceGarbageCollection(): Promise<void> { async forceGarbageCollection(): Promise<void> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
// TODO: consider calling this only when bindings are added.
private async _installMainBinding() {
const functionDeclaration = addMainBinding.toString();
const args: bidi.Script.ChannelValue[] = [{
type: 'channel',
value: {
channel: kPlaywrightBindingChannel,
ownership: bidi.Script.ResultOwnership.Root,
}
}];
const promises = [];
promises.push(this._session.send('script.addPreloadScript', {
functionDeclaration,
arguments: args,
}));
promises.push(this._session.send('script.callFunction', {
functionDeclaration,
arguments: args,
target: toBidiExecutionContext(await this._page.mainFrame()._mainContext())._target,
awaitPromise: false,
userActivation: false,
}));
await Promise.all(promises);
}
private async _onScriptMessage(event: bidi.Script.MessageParameters) {
if (event.channel !== kPlaywrightBindingChannel)
return;
const pageOrError = await this.pageOrError();
if (pageOrError instanceof Error)
return;
const context = this._realmToContext.get(event.source.realm);
if (!context)
return;
if (event.data.type !== 'string')
return;
await this._page._onBindingCalled(event.data.value, context);
}
async addInitScript(initScript: InitScript): Promise<void> { async addInitScript(initScript: InitScript): Promise<void> {
const { script } = await this._session.send('script.addPreloadScript', { const { script } = await this._session.send('script.addPreloadScript', {
// TODO: remove function call from the source. // TODO: remove function call from the source.
@ -355,7 +404,20 @@ export class BidiPage implements PageDelegate {
} }
async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> { 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.'); const rect = (documentRect || viewportRect)!;
const { data } = await this._session.send('browsingContext.captureScreenshot', {
context: this._session.sessionId,
format: {
type: `image/${format === 'png' ? 'png' : 'jpeg'}`,
quality: quality || 80,
},
origin: documentRect ? 'document' : 'viewport',
clip: {
type: 'box',
...rect,
}
});
return Buffer.from(data, 'base64');
} }
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> { async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
@ -522,6 +584,10 @@ export class BidiPage implements PageDelegate {
} }
} }
function addMainBinding(callback: (arg: any) => void) {
(globalThis as any)['__playwright__binding__'] = callback;
}
function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext { function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext {
return (executionContext as any)[contextDelegateSymbol] as BidiExecutionContext; return (executionContext as any)[contextDelegateSymbol] as BidiExecutionContext;
} }

View file

@ -131,15 +131,15 @@ 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, RecorderApp.factory(this), { pauseOnNextStatement: true }); await Recorder.show('actions', 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, RecorderApp.factory(this)); Recorder.showInspectorNoReply(this, RecorderApp.factory(this));
this._debugger.on(Debugger.Events.PausedStateChanged, () => { this._debugger.on(Debugger.Events.PausedStateChanged, () => {
if (this._debugger.isPaused()) if (this._debugger.isPaused())
Recorder.showInspector(this, RecorderApp.factory(this)); Recorder.showInspectorNoReply(this, RecorderApp.factory(this));
}); });
if (debugMode() === 'console') if (debugMode() === 'console')
@ -525,7 +525,7 @@ export abstract class BrowserContext extends SdkObject {
const internalMetadata = serverSideCallMetadata(); const internalMetadata = serverSideCallMetadata();
const page = await this.newPage(internalMetadata); const page = await this.newPage(internalMetadata);
await page._setServerRequestInterceptor(handler => { await page._setServerRequestInterceptor(handler => {
handler.fulfill({ body: '<html></html>', requestUrl: handler.request().url() }).catch(() => {}); handler.fulfill({ body: '<html></html>' }).catch(() => {});
return true; return true;
}); });
for (const origin of originsToSave) { for (const origin of originsToSave) {
@ -559,7 +559,7 @@ export abstract class BrowserContext extends SdkObject {
isServerSide: false, isServerSide: false,
}); });
await page._setServerRequestInterceptor(handler => { await page._setServerRequestInterceptor(handler => {
handler.fulfill({ body: '<html></html>', requestUrl: handler.request().url() }).catch(() => {}); handler.fulfill({ body: '<html></html>' }).catch(() => {});
return true; return true;
}); });
@ -594,7 +594,7 @@ export abstract class BrowserContext extends SdkObject {
const internalMetadata = serverSideCallMetadata(); const internalMetadata = serverSideCallMetadata();
const page = await this.newPage(internalMetadata); const page = await this.newPage(internalMetadata);
await page._setServerRequestInterceptor(handler => { await page._setServerRequestInterceptor(handler => {
handler.fulfill({ body: '<html></html>', requestUrl: handler.request().url() }).catch(() => {}); handler.fulfill({ body: '<html></html>' }).catch(() => {});
return true; return true;
}); });
for (const originState of state.origins) { for (const originState of state.origins) {

View file

@ -35,8 +35,8 @@ export const chromiumSwitches = [
// Translate - https://github.com/microsoft/playwright/issues/16126 // Translate - https://github.com/microsoft/playwright/issues/16126
// HttpsUpgrades - https://github.com/microsoft/playwright/pull/27605 // HttpsUpgrades - https://github.com/microsoft/playwright/pull/27605
// PaintHolding - https://github.com/microsoft/playwright/issues/28023 // PaintHolding - https://github.com/microsoft/playwright/issues/28023
// PlzDedicatedWorker - https://github.com/microsoft/playwright/issues/31747 // ThirdPartyStoragePartitioning - https://github.com/microsoft/playwright/issues/32230
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,PlzDedicatedWorker', '--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,ThirdPartyStoragePartitioning',
'--allow-pre-commit-input', '--allow-pre-commit-input',
'--disable-hang-monitor', '--disable-hang-monitor',
'--disable-ipc-flooding-protection', '--disable-ipc-flooding-protection',

View file

@ -840,7 +840,7 @@ CORS RFC1918 enforcement.
resourceIPAddressSpace?: Network.IPAddressSpace; resourceIPAddressSpace?: Network.IPAddressSpace;
clientSecurityState?: Network.ClientSecurityState; clientSecurityState?: Network.ClientSecurityState;
} }
export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader"; export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader"|"NavigationRegistrationUniqueScopeAlreadySet";
export type SharedDictionaryError = "UseErrorCrossOriginNoCorsRequest"|"UseErrorDictionaryLoadFailure"|"UseErrorMatchingDictionaryNotUsed"|"UseErrorUnexpectedContentDictionaryHeader"|"WriteErrorCossOriginNoCorsRequest"|"WriteErrorDisallowedBySettings"|"WriteErrorExpiredResponse"|"WriteErrorFeatureDisabled"|"WriteErrorInsufficientResources"|"WriteErrorInvalidMatchField"|"WriteErrorInvalidStructuredHeader"|"WriteErrorNavigationRequest"|"WriteErrorNoMatchField"|"WriteErrorNonListMatchDestField"|"WriteErrorNonSecureContext"|"WriteErrorNonStringIdField"|"WriteErrorNonStringInMatchDestList"|"WriteErrorNonStringMatchField"|"WriteErrorNonTokenTypeField"|"WriteErrorRequestAborted"|"WriteErrorShuttingDown"|"WriteErrorTooLongIdField"|"WriteErrorUnsupportedType"; export type SharedDictionaryError = "UseErrorCrossOriginNoCorsRequest"|"UseErrorDictionaryLoadFailure"|"UseErrorMatchingDictionaryNotUsed"|"UseErrorUnexpectedContentDictionaryHeader"|"WriteErrorCossOriginNoCorsRequest"|"WriteErrorDisallowedBySettings"|"WriteErrorExpiredResponse"|"WriteErrorFeatureDisabled"|"WriteErrorInsufficientResources"|"WriteErrorInvalidMatchField"|"WriteErrorInvalidStructuredHeader"|"WriteErrorNavigationRequest"|"WriteErrorNoMatchField"|"WriteErrorNonListMatchDestField"|"WriteErrorNonSecureContext"|"WriteErrorNonStringIdField"|"WriteErrorNonStringInMatchDestList"|"WriteErrorNonStringMatchField"|"WriteErrorNonTokenTypeField"|"WriteErrorRequestAborted"|"WriteErrorShuttingDown"|"WriteErrorTooLongIdField"|"WriteErrorUnsupportedType";
/** /**
* Details for issues around "Attribution Reporting API" usage. * Details for issues around "Attribution Reporting API" usage.
@ -1534,7 +1534,7 @@ events afterwards if enabled and recording.
*/ */
windowState?: WindowState; windowState?: WindowState;
} }
export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"windowManagement"; export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"windowManagement";
export type PermissionSetting = "granted"|"denied"|"prompt"; export type PermissionSetting = "granted"|"denied"|"prompt";
/** /**
* Definition of PermissionDescriptor defined in the Permissions API: * Definition of PermissionDescriptor defined in the Permissions API:
@ -3561,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"|"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"; export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"column"|"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"|"placeholder"|"file-selector-button"|"details-content"|"select-fallback-button"|"select-fallback-button-text"|"picker";
/** /**
* Shadow root type. * Shadow root type.
*/ */
@ -3710,6 +3710,7 @@ The property is always undefined now.
isSVG?: boolean; isSVG?: boolean;
compatibilityMode?: CompatibilityMode; compatibilityMode?: CompatibilityMode;
assignedSlot?: BackendNode; assignedSlot?: BackendNode;
isScrollable?: boolean;
} }
/** /**
* A structure to hold the top-level node of a detached tree and an array of its retained descendants. * A structure to hold the top-level node of a detached tree and an array of its retained descendants.
@ -3954,6 +3955,19 @@ The property is always undefined now.
* Called when top layer elements are changed. * Called when top layer elements are changed.
*/ */
export type topLayerElementsUpdatedPayload = void; export type topLayerElementsUpdatedPayload = void;
/**
* Fired when a node's scrollability state changes.
*/
export type scrollableFlagUpdatedPayload = {
/**
* The id of the node.
*/
nodeId: DOM.NodeId;
/**
* If the node is scrollable.
*/
isScrollable: boolean;
}
/** /**
* Called when a pseudo element is removed from an element. * Called when a pseudo element is removed from an element.
*/ */
@ -8102,8 +8116,25 @@ or hexadecimal (0x prefixed) string.
*/ */
size: number; size: number;
} }
/**
* DOM object counter data.
*/
export interface DOMCounter {
/**
* Object name. Note: object names should be presumed volatile and clients should not expect
the returned names to be consistent across runs.
*/
name: string;
/**
* Object count.
*/
count: number;
}
/**
* Retruns current DOM object counters.
*/
export type getDOMCountersParameters = { export type getDOMCountersParameters = {
} }
export type getDOMCountersReturnValue = { export type getDOMCountersReturnValue = {
@ -8111,6 +8142,21 @@ or hexadecimal (0x prefixed) string.
nodes: number; nodes: number;
jsEventListeners: number; jsEventListeners: number;
} }
/**
* Retruns DOM object counters after preparing renderer for leak detection.
*/
export type getDOMCountersForLeakDetectionParameters = {
}
export type getDOMCountersForLeakDetectionReturnValue = {
/**
* DOM object counters.
*/
counters: DOMCounter[];
}
/**
* Prepares for leak detection by terminating workers, stopping spellcheckers,
dropping non-essential internal caches, running garbage collections, etc.
*/
export type prepareForLeakDetectionParameters = { export type prepareForLeakDetectionParameters = {
} }
export type prepareForLeakDetectionReturnValue = { export type prepareForLeakDetectionReturnValue = {
@ -8902,7 +8948,7 @@ This is a temporary ability and it will be removed in the future.
/** /**
* Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request. * Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request.
*/ */
export type CookieExemptionReason = "None"|"UserSetting"|"TPCDMetadata"|"TPCDDeprecationTrial"|"TPCDHeuristics"|"EnterprisePolicy"|"StorageAccess"|"TopLevelStorageAccess"|"CorsOptIn"|"Scheme"; export type CookieExemptionReason = "None"|"UserSetting"|"TPCDMetadata"|"TPCDDeprecationTrial"|"TopLevelTPCDDeprecationTrial"|"TPCDHeuristics"|"EnterprisePolicy"|"StorageAccess"|"TopLevelStorageAccess"|"Scheme";
/** /**
* A cookie which was not stored from a response with the corresponding reason. * A cookie which was not stored from a response with the corresponding reason.
*/ */
@ -11452,7 +11498,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"|"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"; 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"|"controlled-frame"|"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"|"popins"|"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-app-installation"|"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.
*/ */
@ -12040,7 +12086,7 @@ https://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-expl
/** /**
* List of not restored reasons for back-forward cache. * List of not restored reasons for back-forward cache.
*/ */
export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient"; export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"ContentDiscarded"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient";
/** /**
* Types of not restored reasons for back-forward cache. * Types of not restored reasons for back-forward cache.
*/ */
@ -12151,6 +12197,16 @@ dependent on the reason:
frameId: FrameId; frameId: FrameId;
reason: "remove"|"swap"; reason: "remove"|"swap";
} }
/**
* Fired before frame subtree is detached. Emitted before any frame of the
subtree is actually detached.
*/
export type frameSubtreeWillBeDetachedPayload = {
/**
* Id of the frame that is the root of the subtree that will be detached.
*/
frameId: FrameId;
}
/** /**
* Fired once navigation of the frame has completed. Frame is now associated with the new loader. * Fired once navigation of the frame has completed. Frame is now associated with the new loader.
*/ */
@ -14250,6 +14306,15 @@ int, only present for source registrations
debugData: AttributionReportingAggregatableDebugReportingData[]; debugData: AttributionReportingAggregatableDebugReportingData[];
aggregationCoordinatorOrigin?: string; aggregationCoordinatorOrigin?: string;
} }
export interface AttributionScopesData {
values: string[];
/**
* number instead of integer because not all uint32 can be represented by
int
*/
limit: number;
maxEventStates: number;
}
export interface AttributionReportingSourceRegistration { export interface AttributionReportingSourceRegistration {
time: Network.TimeSinceEpoch; time: Network.TimeSinceEpoch;
/** /**
@ -14273,8 +14338,9 @@ int, only present for source registrations
triggerDataMatching: AttributionReportingTriggerDataMatching; triggerDataMatching: AttributionReportingTriggerDataMatching;
destinationLimitPriority: SignedInt64AsBase10; destinationLimitPriority: SignedInt64AsBase10;
aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig; aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig;
scopesData?: AttributionScopesData;
} }
export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"destinationPerDayReportingLimitReached"; export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxScopesChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"exceedsMaxEventStatesLimit"|"destinationPerDayReportingLimitReached";
export type AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude"; export type AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude";
export interface AttributionReportingAggregatableValueDictEntry { export interface AttributionReportingAggregatableValueDictEntry {
key: string; key: string;
@ -14317,6 +14383,7 @@ int
sourceRegistrationTimeConfig: AttributionReportingSourceRegistrationTimeConfig; sourceRegistrationTimeConfig: AttributionReportingSourceRegistrationTimeConfig;
triggerContextId?: string; triggerContextId?: string;
aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig; aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig;
scopes: string[];
} }
export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData"; export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData";
export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports"; export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports";
@ -17019,7 +17086,7 @@ status is shared by prefetchStatusUpdated and prerenderStatusUpdated.
* TODO(https://crbug.com/1384419): revisit the list of PrefetchStatus and * TODO(https://crbug.com/1384419): revisit the list of PrefetchStatus and
filter out the ones that aren't necessary to the developers. filter out the ones that aren't necessary to the developers.
*/ */
export type PrefetchStatus = "PrefetchAllowed"|"PrefetchFailedIneligibleRedirect"|"PrefetchFailedInvalidRedirect"|"PrefetchFailedMIMENotSupported"|"PrefetchFailedNetError"|"PrefetchFailedNon2XX"|"PrefetchFailedPerPageLimitExceeded"|"PrefetchEvictedAfterCandidateRemoved"|"PrefetchEvictedForNewerPrefetch"|"PrefetchHeldback"|"PrefetchIneligibleRetryAfter"|"PrefetchIsPrivacyDecoy"|"PrefetchIsStale"|"PrefetchNotEligibleBrowserContextOffTheRecord"|"PrefetchNotEligibleDataSaverEnabled"|"PrefetchNotEligibleExistingProxy"|"PrefetchNotEligibleHostIsNonUnique"|"PrefetchNotEligibleNonDefaultStoragePartition"|"PrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy"|"PrefetchNotEligibleSchemeIsNotHttps"|"PrefetchNotEligibleUserHasCookies"|"PrefetchNotEligibleUserHasServiceWorker"|"PrefetchNotEligibleBatterySaverEnabled"|"PrefetchNotEligiblePreloadingDisabled"|"PrefetchNotFinishedInTime"|"PrefetchNotStarted"|"PrefetchNotUsedCookiesChanged"|"PrefetchProxyNotAvailable"|"PrefetchResponseUsed"|"PrefetchSuccessfulButNotUsed"|"PrefetchNotUsedProbeFailed"; export type PrefetchStatus = "PrefetchAllowed"|"PrefetchFailedIneligibleRedirect"|"PrefetchFailedInvalidRedirect"|"PrefetchFailedMIMENotSupported"|"PrefetchFailedNetError"|"PrefetchFailedNon2XX"|"PrefetchEvictedAfterCandidateRemoved"|"PrefetchEvictedForNewerPrefetch"|"PrefetchHeldback"|"PrefetchIneligibleRetryAfter"|"PrefetchIsPrivacyDecoy"|"PrefetchIsStale"|"PrefetchNotEligibleBrowserContextOffTheRecord"|"PrefetchNotEligibleDataSaverEnabled"|"PrefetchNotEligibleExistingProxy"|"PrefetchNotEligibleHostIsNonUnique"|"PrefetchNotEligibleNonDefaultStoragePartition"|"PrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy"|"PrefetchNotEligibleSchemeIsNotHttps"|"PrefetchNotEligibleUserHasCookies"|"PrefetchNotEligibleUserHasServiceWorker"|"PrefetchNotEligibleBatterySaverEnabled"|"PrefetchNotEligiblePreloadingDisabled"|"PrefetchNotFinishedInTime"|"PrefetchNotStarted"|"PrefetchNotUsedCookiesChanged"|"PrefetchProxyNotAvailable"|"PrefetchResponseUsed"|"PrefetchSuccessfulButNotUsed"|"PrefetchNotUsedProbeFailed";
/** /**
* Information of headers to be displayed when the header mismatch occurred. * Information of headers to be displayed when the header mismatch occurred.
*/ */
@ -20115,6 +20182,7 @@ Error was thrown.
"DOM.inlineStyleInvalidated": DOM.inlineStyleInvalidatedPayload; "DOM.inlineStyleInvalidated": DOM.inlineStyleInvalidatedPayload;
"DOM.pseudoElementAdded": DOM.pseudoElementAddedPayload; "DOM.pseudoElementAdded": DOM.pseudoElementAddedPayload;
"DOM.topLayerElementsUpdated": DOM.topLayerElementsUpdatedPayload; "DOM.topLayerElementsUpdated": DOM.topLayerElementsUpdatedPayload;
"DOM.scrollableFlagUpdated": DOM.scrollableFlagUpdatedPayload;
"DOM.pseudoElementRemoved": DOM.pseudoElementRemovedPayload; "DOM.pseudoElementRemoved": DOM.pseudoElementRemovedPayload;
"DOM.setChildNodes": DOM.setChildNodesPayload; "DOM.setChildNodes": DOM.setChildNodesPayload;
"DOM.shadowRootPopped": DOM.shadowRootPoppedPayload; "DOM.shadowRootPopped": DOM.shadowRootPoppedPayload;
@ -20173,6 +20241,7 @@ Error was thrown.
"Page.frameAttached": Page.frameAttachedPayload; "Page.frameAttached": Page.frameAttachedPayload;
"Page.frameClearedScheduledNavigation": Page.frameClearedScheduledNavigationPayload; "Page.frameClearedScheduledNavigation": Page.frameClearedScheduledNavigationPayload;
"Page.frameDetached": Page.frameDetachedPayload; "Page.frameDetached": Page.frameDetachedPayload;
"Page.frameSubtreeWillBeDetached": Page.frameSubtreeWillBeDetachedPayload;
"Page.frameNavigated": Page.frameNavigatedPayload; "Page.frameNavigated": Page.frameNavigatedPayload;
"Page.documentOpened": Page.documentOpenedPayload; "Page.documentOpened": Page.documentOpenedPayload;
"Page.frameResized": Page.frameResizedPayload; "Page.frameResized": Page.frameResizedPayload;
@ -20539,6 +20608,7 @@ Error was thrown.
"Log.startViolationsReport": Log.startViolationsReportParameters; "Log.startViolationsReport": Log.startViolationsReportParameters;
"Log.stopViolationsReport": Log.stopViolationsReportParameters; "Log.stopViolationsReport": Log.stopViolationsReportParameters;
"Memory.getDOMCounters": Memory.getDOMCountersParameters; "Memory.getDOMCounters": Memory.getDOMCountersParameters;
"Memory.getDOMCountersForLeakDetection": Memory.getDOMCountersForLeakDetectionParameters;
"Memory.prepareForLeakDetection": Memory.prepareForLeakDetectionParameters; "Memory.prepareForLeakDetection": Memory.prepareForLeakDetectionParameters;
"Memory.forciblyPurgeJavaScriptMemory": Memory.forciblyPurgeJavaScriptMemoryParameters; "Memory.forciblyPurgeJavaScriptMemory": Memory.forciblyPurgeJavaScriptMemoryParameters;
"Memory.setPressureNotificationsSuppressed": Memory.setPressureNotificationsSuppressedParameters; "Memory.setPressureNotificationsSuppressed": Memory.setPressureNotificationsSuppressedParameters;
@ -21148,6 +21218,7 @@ Error was thrown.
"Log.startViolationsReport": Log.startViolationsReportReturnValue; "Log.startViolationsReport": Log.startViolationsReportReturnValue;
"Log.stopViolationsReport": Log.stopViolationsReportReturnValue; "Log.stopViolationsReport": Log.stopViolationsReportReturnValue;
"Memory.getDOMCounters": Memory.getDOMCountersReturnValue; "Memory.getDOMCounters": Memory.getDOMCountersReturnValue;
"Memory.getDOMCountersForLeakDetection": Memory.getDOMCountersForLeakDetectionReturnValue;
"Memory.prepareForLeakDetection": Memory.prepareForLeakDetectionReturnValue; "Memory.prepareForLeakDetection": Memory.prepareForLeakDetectionReturnValue;
"Memory.forciblyPurgeJavaScriptMemory": Memory.forciblyPurgeJavaScriptMemoryReturnValue; "Memory.forciblyPurgeJavaScriptMemory": Memory.forciblyPurgeJavaScriptMemoryReturnValue;
"Memory.setPressureNotificationsSuppressed": Memory.setPressureNotificationsSuppressedReturnValue; "Memory.setPressureNotificationsSuppressed": Memory.setPressureNotificationsSuppressedReturnValue;

View file

@ -68,7 +68,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.ContentFrame()`); const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.ContentFrame`);
const subject = `${pageAlias}${locators.join('')}`; const subject = `${pageAlias}${locators.join('')}`;
const signals = toSignalMap(action); const signals = toSignalMap(action);

View file

@ -20,7 +20,6 @@ import type * as types from '../types';
import type { ActionInContext, LanguageGenerator, LanguageGeneratorOptions } from './types'; import type { ActionInContext, LanguageGenerator, LanguageGeneratorOptions } from './types';
export function generateCode(actions: ActionInContext[], languageGenerator: LanguageGenerator, options: LanguageGeneratorOptions) { export function generateCode(actions: ActionInContext[], languageGenerator: LanguageGenerator, options: LanguageGeneratorOptions) {
actions = collapseActions(actions);
const header = languageGenerator.generateHeader(options); const header = languageGenerator.generateHeader(options);
const footer = languageGenerator.generateFooter(options.saveStorage); const footer = languageGenerator.generateFooter(options.saveStorage);
const actionTexts = actions.map(a => languageGenerator.generateAction(a)).filter(Boolean); const actionTexts = actions.map(a => languageGenerator.generateAction(a)).filter(Boolean);
@ -70,6 +69,23 @@ export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModif
return result; return result;
} }
export function fromKeyboardModifiers(modifiers?: types.SmartKeyboardModifier[]): number {
let result = 0;
if (!modifiers)
return result;
if (modifiers.includes('Alt'))
result |= 1;
if (modifiers.includes('Control'))
result |= 2;
if (modifiers.includes('ControlOrMeta'))
result |= 2;
if (modifiers.includes('Meta'))
result |= 4;
if (modifiers.includes('Shift'))
result |= 8;
return result;
}
export function toClickOptionsForSourceCode(action: actions.ClickAction): types.MouseClickOptions { export function toClickOptionsForSourceCode(action: actions.ClickAction): types.MouseClickOptions {
const modifiers = toKeyboardModifiers(action.modifiers); const modifiers = toKeyboardModifiers(action.modifiers);
const options: types.MouseClickOptions = {}; const options: types.MouseClickOptions = {};
@ -84,19 +100,3 @@ export function toClickOptionsForSourceCode(action: actions.ClickAction): types.
options.position = action.position; options.position = action.position;
return options; return options;
} }
function collapseActions(actions: ActionInContext[]): ActionInContext[] {
const result: ActionInContext[] = [];
for (const action of actions) {
const lastAction = result[result.length - 1];
const isSameAction = lastAction && lastAction.action.name === action.action.name && lastAction.frame.pageAlias === action.frame.pageAlias && lastAction.frame.framePath.join('|') === action.frame.framePath.join('|');
const isSameSelector = lastAction && 'selector' in lastAction.action && 'selector' in action.action && action.action.selector === lastAction.action.selector;
const shouldMerge = isSameAction && (action.action.name === 'navigate' || (action.action.name === 'fill' && isSameSelector));
if (!shouldMerge) {
result.push(action);
continue;
}
result[result.length - 1] = action;
}
return result;
}

View file

@ -55,7 +55,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.content_frame()`); const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.content_frame`);
const subject = `${pageAlias}${locators.join('')}`; const subject = `${pageAlias}${locators.join('')}`;
const signals = toSignalMap(action); const signals = toSignalMap(action);

View file

@ -197,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, () => Promise.resolve(new InspectingRecorderApp(this)), { omitCallTracking: true }))); const result = await Promise.all([...contexts].map(c => Recorder.showInspector(c, { omitCallTracking: true }, () => Promise.resolve(new InspectingRecorderApp(this)))));
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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 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/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Safari/537.36 Edg/129.0.6668.42", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Safari/537.36 Edg/130.0.6723.6",
"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/129.0.6668.42 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 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/129.0.6668.42 Safari/537.36 Edg/129.0.6668.42", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Safari/537.36 Edg/130.0.6723.6",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080

View file

@ -39,6 +39,7 @@ export class Dialog extends SdkObject {
this._onHandle = onHandle; this._onHandle = onHandle;
this._defaultValue = defaultValue || ''; this._defaultValue = defaultValue || '';
this._page._frameManager.dialogDidOpen(this); this._page._frameManager.dialogDidOpen(this);
this.instrumentation.onDialog(this);
} }
page() { page() {

View file

@ -1,5 +1,6 @@
[*] [*]
../../common/ ../../common/
../../generated/
../../protocol/ ../../protocol/
../../utils/ ../../utils/
../../zipBundle.ts ../../zipBundle.ts

View file

@ -41,12 +41,14 @@ import { serializeError } from '../errors';
import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { ElementHandleDispatcher } from './elementHandlerDispatcher';
import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer'; import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer';
import { RecorderApp } from '../recorder/recorderApp'; import { RecorderApp } from '../recorder/recorderApp';
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
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;
_type_BrowserContext = true; _type_BrowserContext = true;
private _context: BrowserContext; private _context: BrowserContext;
private _subscriptions = new Set<channels.BrowserContextUpdateSubscriptionParams['event']>(); private _subscriptions = new Set<channels.BrowserContextUpdateSubscriptionParams['event']>();
_webSocketInterceptionPatterns: channels.BrowserContextSetWebSocketInterceptionPatternsParams['patterns'] = [];
constructor(parentScope: DispatcherScope, context: BrowserContext) { constructor(parentScope: DispatcherScope, context: BrowserContext) {
// We will reparent these to the context below. // We will reparent these to the context below.
@ -283,6 +285,12 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
}); });
} }
async setWebSocketInterceptionPatterns(params: channels.PageSetWebSocketInterceptionPatternsParams, metadata: CallMetadata): Promise<void> {
this._webSocketInterceptionPatterns = params.patterns;
if (params.patterns.length)
await WebSocketRouteDispatcher.installIfNeeded(this, this._context);
}
async storageState(params: channels.BrowserContextStorageStateParams, metadata: CallMetadata): Promise<channels.BrowserContextStorageStateResult> { async storageState(params: channels.BrowserContextStorageStateParams, metadata: CallMetadata): Promise<channels.BrowserContextStorageStateResult> {
return await this._context.storageState(); return await this._context.storageState();
} }
@ -292,9 +300,18 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
await this._context.close(params); await this._context.close(params);
} }
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> { async enableRecorder(params: channels.BrowserContextEnableRecorderParams): Promise<void> {
const factory = process.env.PW_RECORDER_IS_TRACE_VIEWER ? RecorderInTraceViewer.factory(this._context) : RecorderApp.factory(this._context); if (params.codegenMode === 'trace-events') {
await Recorder.show(this._context, factory, params); await this._context.tracing.start({
name: 'trace',
snapshots: true,
screenshots: true,
live: true,
});
await Recorder.show('trace-events', this._context, RecorderInTraceViewer.factory(this._context), params);
} else {
await Recorder.show('actions', this._context, RecorderApp.factory(this._context), params);
}
} }
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) { async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {

View file

@ -35,12 +35,14 @@ import { ArtifactDispatcher } from './artifactDispatcher';
import type { Download } from '../download'; import type { Download } from '../download';
import { createGuid, urlMatches } from '../../utils'; import { createGuid, urlMatches } from '../../utils';
import type { BrowserContextDispatcher } from './browserContextDispatcher'; import type { BrowserContextDispatcher } from './browserContextDispatcher';
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, BrowserContextDispatcher> implements channels.PageChannel { export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, BrowserContextDispatcher> implements channels.PageChannel {
_type_EventTarget = true; _type_EventTarget = true;
_type_Page = true; _type_Page = true;
private _page: Page; private _page: Page;
_subscriptions = new Set<channels.PageUpdateSubscriptionParams['event']>(); _subscriptions = new Set<channels.PageUpdateSubscriptionParams['event']>();
_webSocketInterceptionPatterns: channels.PageSetWebSocketInterceptionPatternsParams['patterns'] = [];
static from(parentScope: BrowserContextDispatcher, page: Page): PageDispatcher { static from(parentScope: BrowserContextDispatcher, page: Page): PageDispatcher {
return PageDispatcher.fromNullable(parentScope, page)!; return PageDispatcher.fromNullable(parentScope, page)!;
@ -186,6 +188,12 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
}); });
} }
async setWebSocketInterceptionPatterns(params: channels.PageSetWebSocketInterceptionPatternsParams, metadata: CallMetadata): Promise<void> {
this._webSocketInterceptionPatterns = params.patterns;
if (params.patterns.length)
await WebSocketRouteDispatcher.installIfNeeded(this.parentScope(), this._page);
}
async expectScreenshot(params: channels.PageExpectScreenshotParams, metadata: CallMetadata): Promise<channels.PageExpectScreenshotResult> { async expectScreenshot(params: channels.PageExpectScreenshotParams, metadata: CallMetadata): Promise<channels.PageExpectScreenshotResult> {
const mask: { frame: Frame, selector: string }[] = (params.mask || []).map(({ frame, selector }) => ({ const mask: { frame: Frame, selector: string }[] = (params.mask || []).map(({ frame, selector }) => ({
frame: (frame as FrameDispatcher)._object, frame: (frame as FrameDispatcher)._object,

View file

@ -0,0 +1,150 @@
/**
* 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 { BrowserContext } from '../browserContext';
import type { Frame } from '../frames';
import { Page } from '../page';
import type * as channels from '@protocol/channels';
import { Dispatcher } from './dispatcher';
import { createGuid, urlMatches } from '../../utils';
import { PageDispatcher } from './pageDispatcher';
import type { BrowserContextDispatcher } from './browserContextDispatcher';
import * as webSocketMockSource from '../../generated/webSocketMockSource';
import type * as ws from '../injected/webSocketMock';
import { eventsHelper } from '../../utils/eventsHelper';
const kBindingInstalledSymbol = Symbol('webSocketRouteBindingInstalled');
const kInitScriptInstalledSymbol = Symbol('webSocketRouteInitScriptInstalled');
export class WebSocketRouteDispatcher extends Dispatcher<{ guid: string }, channels.WebSocketRouteChannel, PageDispatcher | BrowserContextDispatcher> implements channels.WebSocketRouteChannel {
_type_WebSocketRoute = true;
private _id: string;
private _frame: Frame;
private static _idToDispatcher = new Map<string, WebSocketRouteDispatcher>();
constructor(scope: PageDispatcher | BrowserContextDispatcher, id: string, url: string, frame: Frame) {
super(scope, { guid: 'webSocketRoute@' + createGuid() }, 'WebSocketRoute', { url });
this._id = id;
this._frame = frame;
this._eventListeners.push(
// When the frame navigates or detaches, there will be no more communication
// from the mock websocket, so pretend like it was closed.
eventsHelper.addEventListener(frame._page, Page.Events.InternalFrameNavigatedToNewDocument, (frame: Frame) => {
if (frame === this._frame)
this._onClose();
}),
eventsHelper.addEventListener(frame._page, Page.Events.FrameDetached, (frame: Frame) => {
if (frame === this._frame)
this._onClose();
}),
eventsHelper.addEventListener(frame._page, Page.Events.Close, () => this._onClose()),
eventsHelper.addEventListener(frame._page, Page.Events.Crash, () => this._onClose()),
);
WebSocketRouteDispatcher._idToDispatcher.set(this._id, this);
(scope as any)._dispatchEvent('webSocketRoute', { webSocketRoute: this });
}
static async installIfNeeded(contextDispatcher: BrowserContextDispatcher, target: Page | BrowserContext) {
const context = target instanceof Page ? target.context() : target;
if (!(context as any)[kBindingInstalledSymbol]) {
(context as any)[kBindingInstalledSymbol] = true;
await context.exposeBinding('__pwWebSocketBinding', false, (source, payload: ws.BindingPayload) => {
if (payload.type === 'onCreate') {
const pageDispatcher = PageDispatcher.fromNullable(contextDispatcher, source.page);
let scope: PageDispatcher | BrowserContextDispatcher | undefined;
if (pageDispatcher && matchesPattern(pageDispatcher, context._options.baseURL, payload.url))
scope = pageDispatcher;
else if (matchesPattern(contextDispatcher, context._options.baseURL, payload.url))
scope = contextDispatcher;
if (scope) {
new WebSocketRouteDispatcher(scope, payload.id, payload.url, source.frame);
} else {
const request: ws.PassthroughRequest = { id: payload.id, type: 'passthrough' };
source.frame.evaluateExpression(`globalThis.__pwWebSocketDispatch(${JSON.stringify(request)})`).catch(() => {});
}
return;
}
const dispatcher = WebSocketRouteDispatcher._idToDispatcher.get(payload.id);
if (payload.type === 'onMessageFromPage')
dispatcher?._dispatchEvent('messageFromPage', { message: payload.data.data, isBase64: payload.data.isBase64 });
if (payload.type === 'onMessageFromServer')
dispatcher?._dispatchEvent('messageFromServer', { message: payload.data.data, isBase64: payload.data.isBase64 });
if (payload.type === 'onClose')
dispatcher?._onClose();
});
}
if (!(target as any)[kInitScriptInstalledSymbol]) {
(target as any)[kInitScriptInstalledSymbol] = true;
await target.addInitScript(`
(() => {
const module = {};
${webSocketMockSource.source}
(module.exports.inject())(globalThis);
})();
`);
}
}
async connect(params: channels.WebSocketRouteConnectParams) {
await this._evaluateAPIRequest({ id: this._id, type: 'connect' });
}
async ensureOpened(params: channels.WebSocketRouteEnsureOpenedParams) {
await this._evaluateAPIRequest({ id: this._id, type: 'ensureOpened' });
}
async sendToPage(params: channels.WebSocketRouteSendToPageParams) {
await this._evaluateAPIRequest({ id: this._id, type: 'sendToPage', data: { data: params.message, isBase64: params.isBase64 } });
}
async sendToServer(params: channels.WebSocketRouteSendToServerParams) {
await this._evaluateAPIRequest({ id: this._id, type: 'sendToServer', data: { data: params.message, isBase64: params.isBase64 } });
}
async close(params: channels.WebSocketRouteCloseParams) {
await this._evaluateAPIRequest({ id: this._id, type: 'close', code: params.code, reason: params.reason, wasClean: true });
}
private async _evaluateAPIRequest(request: ws.APIRequest) {
await this._frame.evaluateExpression(`globalThis.__pwWebSocketDispatch(${JSON.stringify(request)})`).catch(() => {});
}
override _onDispose() {
WebSocketRouteDispatcher._idToDispatcher.delete(this._id);
}
_onClose() {
// We could enter here twice upon page closure:
// - first from the recursive dispose inintiated by PageDispatcher;
// - then from our own page.on('close') listener.
if (this._disposed)
return;
this._dispatchEvent('close');
this._dispose();
}
}
function matchesPattern(dispatcher: PageDispatcher | BrowserContextDispatcher, baseURL: string | undefined, url: string) {
for (const pattern of dispatcher._webSocketInterceptionPatterns || []) {
const urlMatch = pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags) : pattern.glob;
if (urlMatches(baseURL, url, urlMatch))
return true;
}
return false;
}

View file

@ -35,16 +35,25 @@ export class Download {
this._suggestedFilename = suggestedFilename; this._suggestedFilename = suggestedFilename;
page._browserContext._downloads.add(this); page._browserContext._downloads.add(this);
if (suggestedFilename !== undefined) if (suggestedFilename !== undefined)
this._page.emit(Page.Events.Download, this); this._fireDownloadEvent();
}
page(): Page {
return this._page;
} }
_filenameSuggested(suggestedFilename: string) { _filenameSuggested(suggestedFilename: string) {
assert(this._suggestedFilename === undefined); assert(this._suggestedFilename === undefined);
this._suggestedFilename = suggestedFilename; this._suggestedFilename = suggestedFilename;
this._page.emit(Page.Events.Download, this); this._fireDownloadEvent();
} }
suggestedFilename(): string { suggestedFilename(): string {
return this._suggestedFilename!; return this._suggestedFilename!;
} }
private _fireDownloadEvent() {
this._page.instrumentation.onDownload(this._page, this);
this._page.emit(Page.Events.Download, this);
}
} }

View file

@ -41,6 +41,7 @@ import type * as types from './types';
import type { HeadersArray, ProxySettings } from './types'; import type { HeadersArray, ProxySettings } from './types';
import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor'; import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
import type * as har from '@trace/har'; import type * as har from '@trace/har';
import { TLSSocket } from 'tls';
type FetchRequestOptions = { type FetchRequestOptions = {
userAgent: string; userAgent: string;
@ -73,6 +74,9 @@ export type APIRequestFinishedEvent = {
statusMessage: string; statusMessage: string;
body?: Buffer; body?: Buffer;
timings: har.Timings; timings: har.Timings;
serverIPAddress?: string;
serverPort?: number;
securityDetails?: har.SecurityDetails;
}; };
type SendRequestOptions = https.RequestOptions & { type SendRequestOptions = https.RequestOptions & {
@ -259,6 +263,7 @@ export abstract class APIRequestContext extends SdkObject {
try { try {
return await this._sendRequest(progress, url, options, postData); return await this._sendRequest(progress, url, options, postData);
} catch (e) { } catch (e) {
e = rewriteOpenSSLErrorIfNeeded(e);
if (maxRetries === 0) if (maxRetries === 0)
throw e; throw e;
if (i === maxRetries || (options.deadline && monotonicTime() + backoff > options.deadline)) if (i === maxRetries || (options.deadline && monotonicTime() + backoff > options.deadline))
@ -302,6 +307,10 @@ export abstract class APIRequestContext extends SdkObject {
let tcpConnectionAt: number | undefined; let tcpConnectionAt: number | undefined;
let tlsHandshakeAt: number | undefined; let tlsHandshakeAt: number | undefined;
let requestFinishAt: number | undefined; let requestFinishAt: number | undefined;
let serverIPAddress: string | undefined;
let serverPort: number | undefined;
let securityDetails: har.SecurityDetails | undefined;
const request = requestConstructor(url, requestOptions as any, async response => { const request = requestConstructor(url, requestOptions as any, async response => {
const responseAt = monotonicTime(); const responseAt = monotonicTime();
@ -328,6 +337,9 @@ export abstract class APIRequestContext extends SdkObject {
cookies, cookies,
body, body,
timings, timings,
serverIPAddress,
serverPort,
securityDetails,
}; };
this.emit(APIRequestContext.Events.RequestFinished, requestFinishedEvent); this.emit(APIRequestContext.Events.RequestFinished, requestFinishedEvent);
}; };
@ -464,7 +476,7 @@ export abstract class APIRequestContext extends SdkObject {
body.on('data', chunk => chunks.push(chunk)); body.on('data', chunk => chunks.push(chunk));
body.on('end', notifyBodyFinished); body.on('end', notifyBodyFinished);
}); });
request.on('error', error => reject(rewriteOpenSSLErrorIfNeeded(error))); request.on('error', reject);
const disposeListener = () => { const disposeListener = () => {
reject(new Error('Request context disposed.')); reject(new Error('Request context disposed.'));
@ -482,7 +494,23 @@ export abstract class APIRequestContext extends SdkObject {
// non-happy-eyeballs sockets // non-happy-eyeballs sockets
socket.on('lookup', () => { dnsLookupAt = monotonicTime(); }); socket.on('lookup', () => { dnsLookupAt = monotonicTime(); });
socket.on('connect', () => { tcpConnectionAt = monotonicTime(); }); socket.on('connect', () => { tcpConnectionAt = monotonicTime(); });
socket.on('secureConnect', () => { tlsHandshakeAt = monotonicTime(); }); socket.on('secureConnect', () => {
tlsHandshakeAt = monotonicTime();
if (socket instanceof TLSSocket) {
const peerCertificate = socket.getPeerCertificate();
securityDetails = {
protocol: socket.getProtocol() ?? undefined,
subjectName: peerCertificate.subject.CN,
validFrom: new Date(peerCertificate.valid_from).getTime() / 1000,
validTo: new Date(peerCertificate.valid_to).getTime() / 1000,
issuer: peerCertificate.issuer.CN
};
}
});
serverIPAddress = socket.remoteAddress;
serverPort = socket.remotePort;
}); });
request.on('finish', () => { requestFinishAt = monotonicTime(); }); request.on('finish', () => { requestFinishAt = monotonicTime(); });

View file

@ -782,13 +782,16 @@ export class Frame extends SdkObject {
throw new Error(`state: expected one of (attached|detached|visible|hidden)`); throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
return controller.run(async progress => { return controller.run(async progress => {
progress.log(`waiting for ${this._asLocator(selector)}${state === 'attached' ? '' : ' to be ' + state}`); progress.log(`waiting for ${this._asLocator(selector)}${state === 'attached' ? '' : ' to be ' + state}`);
return await this.waitForSelectorInternal(progress, selector, options, scope); return await this.waitForSelectorInternal(progress, selector, true, options, scope);
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async waitForSelectorInternal(progress: Progress, selector: string, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> { async waitForSelectorInternal(progress: Progress, selector: string, performLocatorHandlersCheckpoint: boolean, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
const { state = 'visible' } = options; const { state = 'visible' } = options;
const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => { const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
if (performLocatorHandlersCheckpoint)
await this._page.performLocatorHandlersCheckpoint(progress);
const resolved = await this.selectors.resolveInjectedForSelector(selector, options, scope); const resolved = await this.selectors.resolveInjectedForSelector(selector, options, scope);
progress.throwIfAborted(); progress.throwIfAborted();
if (!resolved) { if (!resolved) {

View file

@ -213,11 +213,19 @@ export class HarTracer {
harEntry.response.httpVersion = event.httpVersion; harEntry.response.httpVersion = event.httpVersion;
harEntry.response.redirectURL = event.headers.location || ''; harEntry.response.redirectURL = event.headers.location || '';
if (!this._options.omitServerIP) {
harEntry.serverIPAddress = event.serverIPAddress;
harEntry._serverPort = event.serverPort;
}
if (!this._options.omitTiming) { if (!this._options.omitTiming) {
harEntry.timings = event.timings; harEntry.timings = event.timings;
this._computeHarEntryTotalTime(harEntry); this._computeHarEntryTotalTime(harEntry);
} }
if (!this._options.omitSecurityDetails)
harEntry._securityDetails = event.securityDetails;
for (let i = 0; i < event.rawHeaders.length; i += 2) { for (let i = 0; i < event.rawHeaders.length; i += 2) {
harEntry.response.headers.push({ harEntry.response.headers.push({
name: event.rawHeaders[i], name: event.rawHeaders[i],
@ -236,6 +244,8 @@ export class HarTracer {
if (contentType) if (contentType)
content.mimeType = contentType; content.mimeType = contentType;
this._storeResponseContent(event.body, content, 'other'); this._storeResponseContent(event.body, content, 'other');
if (!this._options.omitSizes)
harEntry.response.bodySize = event.body?.length ?? 0;
if (this._started) if (this._started)
this._delegate.onEntryFinished(harEntry); this._delegate.onEntryFinished(harEntry);

View file

@ -1039,9 +1039,12 @@ export class Recorder {
this.highlight.install(); this.highlight.install();
// some frameworks erase the DOM on hydration, this ensures it's reattached // some frameworks erase the DOM on hydration, this ensures it's reattached
const recreationInterval = setInterval(() => { let recreationInterval: number | undefined;
const recreate = () => {
this.highlight.install(); this.highlight.install();
}, 500); recreationInterval = this.injectedScript.builtinSetTimeout(recreate, 500);
};
recreationInterval = this.injectedScript.builtinSetTimeout(recreate, 500);
this._listeners.push(() => clearInterval(recreationInterval)); this._listeners.push(() => clearInterval(recreationInterval));
this.overlay?.install(); this.overlay?.install();

View file

@ -0,0 +1,343 @@
/**
* 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.
*/
export type WebSocketMessage = string | ArrayBufferLike | Blob | ArrayBufferView;
export type WSData = { data: string, isBase64: boolean };
export type OnCreatePayload = { type: 'onCreate', id: string, url: string };
export type OnMessageFromPagePayload = { type: 'onMessageFromPage', id: string, data: WSData };
export type OnClosePayload = { type: 'onClose', id: string, code: number | undefined, reason: string | undefined, wasClean: boolean };
export type OnMessageFromServerPayload = { type: 'onMessageFromServer', id: string, data: WSData };
export type BindingPayload = OnCreatePayload | OnMessageFromPagePayload | OnMessageFromServerPayload | OnClosePayload;
export type ConnectRequest = { type: 'connect', id: string };
export type PassthroughRequest = { type: 'passthrough', id: string };
export type EnsureOpenedRequest = { type: 'ensureOpened', id: string };
export type SendToPageRequest = { type: 'sendToPage', id: string, data: WSData };
export type SendToServerRequest = { type: 'sendToServer', id: string, data: WSData };
export type CloseRequest = { type: 'close', id: string, code: number | undefined, reason: string | undefined, wasClean: boolean };
export type APIRequest = ConnectRequest | PassthroughRequest | EnsureOpenedRequest | SendToPageRequest | SendToServerRequest | CloseRequest;
// eslint-disable-next-line no-restricted-globals
type GlobalThis = typeof globalThis;
export function inject(globalThis: GlobalThis) {
if ((globalThis as any).__pwWebSocketDispatch)
return;
function generateId() {
const bytes = new Uint8Array(32);
globalThis.crypto.getRandomValues(bytes);
const hex = '0123456789abcdef';
return [...bytes].map(value => {
const high = Math.floor(value / 16);
const low = value % 16;
return hex[high] + hex[low];
}).join('');
}
function bufferToData(b: Uint8Array): WSData {
let s = '';
for (let i = 0; i < b.length; i++)
s += String.fromCharCode(b[i]);
return { data: globalThis.btoa(s), isBase64: true };
}
function stringToBuffer(s: string): ArrayBuffer {
s = globalThis.atob(s);
const b = new Uint8Array(s.length);
for (let i = 0; i < s.length; i++)
b[i] = s.charCodeAt(i);
return b.buffer;
}
// Note: this function tries to be synchronous when it can to preserve the ability to send
// multiple messages synchronously in the same order and then synchronously close.
function messageToData(message: WebSocketMessage, cb: (data: WSData) => any) {
if (message instanceof globalThis.Blob)
return message.arrayBuffer().then(buffer => cb(bufferToData(new Uint8Array(buffer))));
if (typeof message === 'string')
return cb({ data: message, isBase64: false });
if (ArrayBuffer.isView(message))
return cb(bufferToData(new Uint8Array(message.buffer, message.byteOffset, message.byteLength)));
return cb(bufferToData(new Uint8Array(message)));
}
function dataToMessage(data: WSData, binaryType: 'blob' | 'arraybuffer'): WebSocketMessage {
if (!data.isBase64)
return data.data;
const buffer = stringToBuffer(data.data);
return binaryType === 'arraybuffer' ? buffer : new Blob([buffer]);
}
const binding = (globalThis as any).__pwWebSocketBinding as (message: BindingPayload) => void;
const NativeWebSocket: typeof WebSocket = globalThis.WebSocket;
const idToWebSocket = new Map<string, WebSocketMock>();
(globalThis as any).__pwWebSocketDispatch = (request: APIRequest) => {
const ws = idToWebSocket.get(request.id);
if (!ws)
return;
if (request.type === 'connect')
ws._apiConnect();
if (request.type === 'passthrough')
ws._apiPassThrough();
if (request.type === 'ensureOpened')
ws._apiEnsureOpened();
if (request.type === 'sendToPage')
ws._apiSendToPage(dataToMessage(request.data, ws.binaryType));
if (request.type === 'close')
ws._apiClose(request.code, request.reason, request.wasClean);
if (request.type === 'sendToServer')
ws._apiSendToServer(dataToMessage(request.data, ws.binaryType));
};
class WebSocketMock extends EventTarget {
static readonly CONNECTING: 0 = 0; // WebSocket.CONNECTING
static readonly OPEN: 1 = 1; // WebSocket.OPEN
static readonly CLOSING: 2 = 2; // WebSocket.CLOSING
static readonly CLOSED: 3 = 3; // WebSocket.CLOSED
CONNECTING: 0 = 0; // WebSocket.CONNECTING
OPEN: 1 = 1; // WebSocket.OPEN
CLOSING: 2 = 2; // WebSocket.CLOSING
CLOSED: 3 = 3; // WebSocket.CLOSED
private _oncloseListener: WebSocket['onclose'] = null;
private _onerrorListener: WebSocket['onerror'] = null;
private _onmessageListener: WebSocket['onmessage'] = null;
private _onopenListener: WebSocket['onopen'] = null;
bufferedAmount: number = 0;
extensions: string = '';
protocol: string = '';
readyState: number = 0;
readonly url: string;
private _id: string;
private _origin: string = '';
private _protocols?: string | string[];
private _ws?: WebSocket;
private _passthrough = false;
private _wsBufferedMessages: WebSocketMessage[] = [];
private _binaryType: BinaryType = 'blob';
constructor(url: string | URL, protocols?: string | string[]) {
super();
this.url = typeof url === 'string' ? url : url.href;
try {
this._origin = new URL(url).origin;
} catch {
}
this._protocols = protocols;
this._id = generateId();
idToWebSocket.set(this._id, this);
binding({ type: 'onCreate', id: this._id, url: this.url });
}
// --- native WebSocket implementation ---
get binaryType() {
return this._binaryType;
}
set binaryType(type) {
this._binaryType = type;
if (this._ws)
this._ws.binaryType = type;
}
get onclose() {
return this._oncloseListener;
}
set onclose(listener) {
if (this._oncloseListener)
this.removeEventListener('close', this._oncloseListener as any);
this._oncloseListener = listener;
if (this._oncloseListener)
this.addEventListener('close', this._oncloseListener as any);
}
get onerror() {
return this._onerrorListener;
}
set onerror(listener) {
if (this._onerrorListener)
this.removeEventListener('error', this._onerrorListener);
this._onerrorListener = listener;
if (this._onerrorListener)
this.addEventListener('error', this._onerrorListener);
}
get onopen() {
return this._onopenListener;
}
set onopen(listener) {
if (this._onopenListener)
this.removeEventListener('open', this._onopenListener);
this._onopenListener = listener;
if (this._onopenListener)
this.addEventListener('open', this._onopenListener);
}
get onmessage() {
return this._onmessageListener;
}
set onmessage(listener) {
if (this._onmessageListener)
this.removeEventListener('message', this._onmessageListener as any);
this._onmessageListener = listener;
if (this._onmessageListener)
this.addEventListener('message', this._onmessageListener as any);
}
send(message: WebSocketMessage): void {
if (this.readyState === WebSocketMock.CONNECTING)
throw new DOMException(`Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.`);
if (this.readyState !== WebSocketMock.OPEN)
throw new DOMException(`WebSocket is already in CLOSING or CLOSED state.`);
if (this._passthrough)
this._apiSendToServer(message);
else
messageToData(message, data => binding({ type: 'onMessageFromPage', id: this._id, data }));
}
close(code?: number, reason?: string): void {
if (code !== undefined && code !== 1000 && (code < 3000 || code > 4999))
throw new DOMException(`Failed to execute 'close' on 'WebSocket': The close code must be either 1000, or between 3000 and 4999. ${code} is neither.`);
if (this.readyState === WebSocketMock.OPEN || this.readyState === WebSocketMock.CONNECTING)
this.readyState = WebSocketMock.CLOSING;
if (this._ws)
this._ws.close(code, reason);
else
this._onWSClose(code, reason, true);
}
// --- methods called from the routing API ---
_apiEnsureOpened() {
// This is called at the end of the route handler. If we did not connect to the server,
// assume that websocket will be fully mocked. In this case, pretend that server
// connection is established right away.
if (!this._ws)
this._ensureOpened();
}
_apiSendToPage(message: WebSocketMessage) {
// Calling "sendToPage()" from the route handler. Allow this for easier testing.
this._ensureOpened();
if (this.readyState !== WebSocketMock.OPEN)
throw new DOMException(`WebSocket is already in CLOSING or CLOSED state.`);
this.dispatchEvent(new MessageEvent('message', { data: message, origin: this._origin, cancelable: true }));
}
_apiSendToServer(message: WebSocketMessage) {
if (!this._ws)
throw new Error('Cannot send a message before connecting to the server');
if (this._ws.readyState === WebSocketMock.CONNECTING)
this._wsBufferedMessages.push(message);
else
this._ws.send(message);
}
_apiConnect() {
if (this._ws)
throw new Error('Can only connect to the server once');
this._ws = new NativeWebSocket(this.url, this._protocols);
this._ws.binaryType = this._binaryType;
this._ws.onopen = () => {
for (const message of this._wsBufferedMessages)
this._ws!.send(message);
this._wsBufferedMessages = [];
this._ensureOpened();
};
this._ws.onclose = event => {
this._onWSClose(event.code, event.reason, event.wasClean);
};
this._ws.onmessage = event => {
if (this._passthrough)
this._apiSendToPage(event.data);
else
messageToData(event.data, data => binding({ type: 'onMessageFromServer', id: this._id, data }));
};
this._ws.onerror = () => {
// We do not expose errors in the API, so short-curcuit the error event.
const event = new Event('error', { cancelable: true });
this.dispatchEvent(event);
};
}
// This method connects to the server, and passes all messages through,
// as if WebSocketMock was not engaged.
_apiPassThrough() {
this._passthrough = true;
this._apiConnect();
}
_apiClose(code: number | undefined, reason: string | undefined, wasClean: boolean) {
if (this.readyState !== WebSocketMock.CLOSED) {
this.readyState = WebSocketMock.CLOSED;
this.dispatchEvent(new CloseEvent('close', { code, reason, wasClean, cancelable: true }));
}
// Immediately close the real WS and imitate that it has closed.
this._ws?.close(code, reason);
this._cleanupWS();
binding({ type: 'onClose', id: this._id, code, reason, wasClean });
idToWebSocket.delete(this._id);
}
// --- internals ---
_ensureOpened() {
if (this.readyState !== WebSocketMock.CONNECTING)
return;
this.readyState = WebSocketMock.OPEN;
this.dispatchEvent(new Event('open', { cancelable: true }));
}
private _onWSClose(code: number | undefined, reason: string | undefined, wasClean: boolean) {
this._cleanupWS();
if (this.readyState !== WebSocketMock.CLOSED) {
this.readyState = WebSocketMock.CLOSED;
this.dispatchEvent(new CloseEvent('close', { code, reason, wasClean, cancelable: true }));
}
binding({ type: 'onClose', id: this._id, code, reason, wasClean });
idToWebSocket.delete(this._id);
}
private _cleanupWS() {
if (!this._ws)
return;
this._ws.onopen = null;
this._ws.onclose = null;
this._ws.onmessage = null;
this._ws.onerror = null;
this._ws = undefined;
this._wsBufferedMessages = [];
}
}
globalThis.WebSocket = class WebSocket extends WebSocketMock {};
}

View file

@ -35,6 +35,8 @@ export type Attribution = {
}; };
import type { CallMetadata } from '@protocol/callMetadata'; import type { CallMetadata } from '@protocol/callMetadata';
import type { Dialog } from './dialog';
import type { Download } from './download';
export type { CallMetadata } from '@protocol/callMetadata'; export type { CallMetadata } from '@protocol/callMetadata';
export class SdkObject extends EventEmitter { export class SdkObject extends EventEmitter {
@ -62,6 +64,8 @@ export interface Instrumentation {
onPageClose(page: Page): void; onPageClose(page: Page): void;
onBrowserOpen(browser: Browser): void; onBrowserOpen(browser: Browser): void;
onBrowserClose(browser: Browser): void; onBrowserClose(browser: Browser): void;
onDialog(dialog: Dialog): void;
onDownload(page: Page, download: Download): void;
} }
export interface InstrumentationListener { export interface InstrumentationListener {
@ -73,6 +77,8 @@ export interface InstrumentationListener {
onPageClose?(page: Page): void; onPageClose?(page: Page): void;
onBrowserOpen?(browser: Browser): void; onBrowserOpen?(browser: Browser): void;
onBrowserClose?(browser: Browser): void; onBrowserClose?(browser: Browser): void;
onDialog?(dialog: Dialog): void;
onDownload?(page: Page, download: Download): void;
} }
export function createInstrumentation(): Instrumentation { export function createInstrumentation(): Instrumentation {

View file

@ -473,7 +473,7 @@ export class Page extends SdkObject {
progress.throwIfAborted(); progress.throwIfAborted();
if (!handler.noWaitAfter) { if (!handler.noWaitAfter) {
progress.log(` locator handler has finished, waiting for ${asLocator(this.attribution.playwright.options.sdkLanguage, handler.selector)} to be hidden`); progress.log(` locator handler has finished, waiting for ${asLocator(this.attribution.playwright.options.sdkLanguage, handler.selector)} to be hidden`);
await this.mainFrame().waitForSelectorInternal(progress, handler.selector, { state: 'hidden' }); await this.mainFrame().waitForSelectorInternal(progress, handler.selector, false, { state: 'hidden' });
} else { } else {
progress.log(` locator handler has finished`); progress.log(` locator handler has finished`);
} }

View file

@ -45,32 +45,35 @@ export class Recorder implements InstrumentationListener, IRecorder {
private _omitCallTracking = false; private _omitCallTracking = false;
private _currentLanguage: Language; private _currentLanguage: Language;
static showInspector(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) { static async showInspector(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, recorderAppFactory: IRecorderAppFactory) {
const params: channels.BrowserContextRecorderSupplementEnableParams = {};
if (isUnderTest()) if (isUnderTest())
params.language = process.env.TEST_INSPECTOR_LANGUAGE; params.language = process.env.TEST_INSPECTOR_LANGUAGE;
Recorder.show(context, recorderAppFactory, params).catch(() => {}); return await Recorder.show('actions', context, recorderAppFactory, params);
} }
static show(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<Recorder> { static showInspectorNoReply(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) {
Recorder.showInspector(context, {}, recorderAppFactory).catch(() => {});
}
static show(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise<Recorder> {
let recorderPromise = (context as any)[recorderSymbol] as Promise<Recorder>; let recorderPromise = (context as any)[recorderSymbol] as Promise<Recorder>;
if (!recorderPromise) { if (!recorderPromise) {
recorderPromise = Recorder._create(context, recorderAppFactory, params); recorderPromise = Recorder._create(codegenMode, context, recorderAppFactory, params);
(context as any)[recorderSymbol] = recorderPromise; (context as any)[recorderSymbol] = recorderPromise;
} }
return recorderPromise; return recorderPromise;
} }
private static async _create(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<Recorder> { private static async _create(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise<Recorder> {
const recorder = new Recorder(context, params); const recorder = new Recorder(codegenMode, context, params);
const recorderApp = await recorderAppFactory(recorder); const recorderApp = await recorderAppFactory(recorder);
await recorder._install(recorderApp); await recorder._install(recorderApp);
return recorder; return recorder;
} }
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
this._mode = params.mode || 'none'; this._mode = params.mode || 'none';
this._contextRecorder = new ContextRecorder(context, params, {}); this._contextRecorder = new ContextRecorder(codegenMode, context, params, {});
this._context = context; this._context = context;
this._omitCallTracking = !!params.omitCallTracking; this._omitCallTracking = !!params.omitCallTracking;
this._debugger = context.debugger(); this._debugger = context.debugger();

View file

@ -48,14 +48,14 @@ export class ContextRecorder extends EventEmitter {
private _lastDialogOrdinal = -1; private _lastDialogOrdinal = -1;
private _lastDownloadOrdinal = -1; private _lastDownloadOrdinal = -1;
private _context: BrowserContext; private _context: BrowserContext;
private _params: channels.BrowserContextRecorderSupplementEnableParams; private _params: channels.BrowserContextEnableRecorderParams;
private _delegate: ContextRecorderDelegate; private _delegate: ContextRecorderDelegate;
private _recorderSources: Source[]; private _recorderSources: Source[];
private _throttledOutputFile: ThrottledFile | null = null; private _throttledOutputFile: ThrottledFile | null = null;
private _orderedLanguages: LanguageGenerator[] = []; private _orderedLanguages: LanguageGenerator[] = [];
private _listeners: RegisteredListener[] = []; private _listeners: RegisteredListener[] = [];
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams, delegate: ContextRecorderDelegate) { constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) {
super(); super();
this._context = context; this._context = context;
this._params = params; this._params = params;
@ -73,11 +73,11 @@ export class ContextRecorder extends EventEmitter {
saveStorage: params.saveStorage, saveStorage: params.saveStorage,
}; };
const collection = new RecorderCollection(this._pageAliases, params.mode === 'recording'); this._collection = new RecorderCollection(codegenMode, context, this._pageAliases);
collection.on('change', () => { this._collection.on('change', (actions: ActionInContext[]) => {
this._recorderSources = []; this._recorderSources = [];
for (const languageGenerator of this._orderedLanguages) { for (const languageGenerator of this._orderedLanguages) {
const { header, footer, actionTexts, text } = generateCode(collection.actions(), languageGenerator, languageGeneratorOptions); const { header, footer, actionTexts, text } = generateCode(actions, languageGenerator, languageGeneratorOptions);
const source: Source = { const source: Source = {
isRecorded: true, isRecorded: true,
label: languageGenerator.name, label: languageGenerator.name,
@ -103,7 +103,7 @@ export class ContextRecorder extends EventEmitter {
this._listeners.push(eventsHelper.addEventListener(process, 'exit', () => { this._listeners.push(eventsHelper.addEventListener(process, 'exit', () => {
this._throttledOutputFile?.flush(); this._throttledOutputFile?.flush();
})); }));
this._collection = collection; this.setEnabled(true);
} }
setOutput(codegenId: string, outputFile?: string) { setOutput(codegenId: string, outputFile?: string) {
@ -145,6 +145,10 @@ export class ContextRecorder extends EventEmitter {
setEnabled(enabled: boolean) { setEnabled(enabled: boolean) {
this._collection.setEnabled(enabled); this._collection.setEnabled(enabled);
if (enabled)
this._context.tracing.startChunk({ name: 'trace', title: 'trace' }).catch(() => {});
else
this._context.tracing.stopChunk({ mode: 'discard' }).catch(() => {});
} }
dispose() { dispose() {

View file

@ -43,6 +43,7 @@ declare global {
} }
export class EmptyRecorderApp extends EventEmitter implements IRecorderApp { export class EmptyRecorderApp extends EventEmitter implements IRecorderApp {
wsEndpointForTest: undefined;
async close(): Promise<void> {} async close(): Promise<void> {}
async setPaused(paused: boolean): Promise<void> {} async setPaused(paused: boolean): Promise<void> {}
async setMode(mode: Mode): Promise<void> {} async setMode(mode: Mode): Promise<void> {}
@ -54,7 +55,7 @@ export class EmptyRecorderApp extends EventEmitter implements IRecorderApp {
export class RecorderApp extends EventEmitter implements IRecorderApp { export class RecorderApp extends EventEmitter implements IRecorderApp {
private _page: Page; private _page: Page;
readonly wsEndpoint: string | undefined; readonly wsEndpointForTest: string | undefined;
private _recorder: IRecorder; private _recorder: IRecorder;
constructor(recorder: IRecorder, page: Page, wsEndpoint: string | undefined) { constructor(recorder: IRecorder, page: Page, wsEndpoint: string | undefined) {
@ -62,7 +63,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
this.setMaxListeners(0); this.setMaxListeners(0);
this._recorder = recorder; this._recorder = recorder;
this._page = page; this._page = page;
this.wsEndpoint = wsEndpoint; this.wsEndpointForTest = wsEndpoint;
} }
async close() { async close() {
@ -80,7 +81,6 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
const file = require.resolve('../../vite/recorder/' + uri); const file = require.resolve('../../vite/recorder/' + uri);
fs.promises.readFile(file).then(buffer => { fs.promises.readFile(file).then(buffer => {
route.fulfill({ route.fulfill({
requestUrl: route.request().url(),
status: 200, status: 200,
headers: [ headers: [
{ name: 'Content-Type', value: mime.getType(path.extname(file)) || 'application/octet-stream' } { name: 'Content-Type', value: mime.getType(path.extname(file)) || 'application/octet-stream' }
@ -122,9 +122,8 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
persistentContextOptions: { persistentContextOptions: {
noDefaultViewport: true, noDefaultViewport: true,
headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed), headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed),
useWebSocket: !!process.env.PWTEST_RECORDER_PORT, useWebSocket: isUnderTest(),
handleSIGINT: false, handleSIGINT: false,
args: process.env.PWTEST_RECORDER_PORT ? [`--remote-debugging-port=${process.env.PWTEST_RECORDER_PORT}`] : [],
executablePath: inspectedContext._browser.options.isChromium ? inspectedContext._browser.options.customExecutablePath : undefined, executablePath: inspectedContext._browser.options.isChromium ? inspectedContext._browser.options.customExecutablePath : undefined,
} }
}); });
@ -162,8 +161,10 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
}).toString(), { isFunction: true }, sources).catch(() => {}); }).toString(), { isFunction: true }, sources).catch(() => {});
// Testing harness for runCLI mode. // Testing harness for runCLI mode.
if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) {
(process as any)._didSetSourcesForTest(sources[0].text); if ((process as any)._didSetSourcesForTest(sources[0].text))
this.close();
}
} }
async setSelector(selector: string, userGesture?: boolean): Promise<void> { async setSelector(selector: string, userGesture?: boolean): Promise<void> {

View file

@ -20,31 +20,35 @@ import type { Page } from '../page';
import type { Signal } from './recorderActions'; import type { Signal } from './recorderActions';
import type { ActionInContext } from '../codegen/types'; import type { ActionInContext } from '../codegen/types';
import { monotonicTime } from '../../utils/time'; import { monotonicTime } from '../../utils/time';
import { callMetadataForAction } from './recorderUtils'; import { callMetadataForAction, collapseActions, traceEventsToAction } from './recorderUtils';
import { serializeError } from '../errors'; import { serializeError } from '../errors';
import { performAction } from './recorderRunner'; import { performAction } from './recorderRunner';
import type { CallMetadata } from '@protocol/callMetadata'; import type { CallMetadata } from '@protocol/callMetadata';
import { isUnderTest } from '../../utils/debug'; import { isUnderTest } from '../../utils/debug';
import type { BrowserContext } from '../browserContext';
export class RecorderCollection extends EventEmitter { export class RecorderCollection extends EventEmitter {
private _actions: ActionInContext[] = []; private _actions: ActionInContext[] = [];
private _enabled: boolean; private _enabled = false;
private _pageAliases: Map<Page, string>; private _pageAliases: Map<Page, string>;
private _context: BrowserContext;
constructor(pageAliases: Map<Page, string>, enabled: boolean) { constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, pageAliases: Map<Page, string>) {
super(); super();
this._enabled = enabled; this._context = context;
this._pageAliases = pageAliases; this._pageAliases = pageAliases;
this.restart();
if (codegenMode === 'trace-events') {
this._context.tracing.onMemoryEvents(events => {
this._actions = traceEventsToAction(events);
this._fireChange();
});
}
} }
restart() { restart() {
this._actions = []; this._actions = [];
this.emit('change'); this._fireChange();
}
actions() {
return this._actions;
} }
setEnabled(enabled: boolean) { setEnabled(enabled: boolean) {
@ -60,7 +64,7 @@ export class RecorderCollection extends EventEmitter {
addRecordedAction(actionInContext: ActionInContext) { addRecordedAction(actionInContext: ActionInContext) {
if (['openPage', 'closePage'].includes(actionInContext.action.name)) { if (['openPage', 'closePage'].includes(actionInContext.action.name)) {
this._actions.push(actionInContext); this._actions.push(actionInContext);
this.emit('change'); this._fireChange();
return; return;
} }
this._addAction(actionInContext).catch(() => {}); this._addAction(actionInContext).catch(() => {});
@ -69,11 +73,16 @@ export class RecorderCollection extends EventEmitter {
private async _addAction(actionInContext: ActionInContext, callback?: (callMetadata: CallMetadata) => Promise<void>) { private async _addAction(actionInContext: ActionInContext, callback?: (callMetadata: CallMetadata) => Promise<void>) {
if (!this._enabled) if (!this._enabled)
return; return;
if (actionInContext.action.name === 'openPage' || actionInContext.action.name === 'closePage') {
this._actions.push(actionInContext);
this._fireChange();
return;
}
const { callMetadata, mainFrame } = callMetadataForAction(this._pageAliases, actionInContext); const { callMetadata, mainFrame } = callMetadataForAction(this._pageAliases, actionInContext);
await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata); await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata);
this._actions.push(actionInContext); this._actions.push(actionInContext);
this.emit('change'); this._fireChange();
const error = await callback?.(callMetadata).catch((e: Error) => e); const error = await callback?.(callMetadata).catch((e: Error) => e);
callMetadata.endTime = monotonicTime(); callMetadata.endTime = monotonicTime();
callMetadata.error = error ? serializeError(error) : undefined; callMetadata.error = error ? serializeError(error) : undefined;
@ -116,8 +125,12 @@ export class RecorderCollection extends EventEmitter {
if (this._actions.length) { if (this._actions.length) {
this._actions[this._actions.length - 1].action.signals.push(signal); this._actions[this._actions.length - 1].action.signals.push(signal);
this.emit('change'); this._fireChange();
return; return;
} }
} }
private _fireChange() {
this.emit('change', collapseActions(this._actions));
}
} }

View file

@ -23,6 +23,7 @@ export interface IRecorder {
} }
export interface IRecorderApp extends EventEmitter { export interface IRecorderApp extends EventEmitter {
readonly wsEndpointForTest: string | undefined;
close(): Promise<void>; close(): Promise<void>;
setPaused(paused: boolean): Promise<void>; setPaused(paused: boolean): Promise<void>;
setMode(mode: Mode): Promise<void>; setMode(mode: Mode): Promise<void>;

View file

@ -21,74 +21,97 @@ import type { IRecorder, IRecorderApp, IRecorderAppFactory } from './recorderFro
import { installRootRedirect, openTraceViewerApp, startTraceViewerServer } from '../trace/viewer/traceViewer'; import { installRootRedirect, openTraceViewerApp, startTraceViewerServer } from '../trace/viewer/traceViewer';
import type { TraceViewerServerOptions } from '../trace/viewer/traceViewer'; import type { TraceViewerServerOptions } from '../trace/viewer/traceViewer';
import type { BrowserContext } from '../browserContext'; import type { BrowserContext } from '../browserContext';
import { gracefullyProcessExitDoNotHang } from '../../utils/processLauncher'; import type { HttpServer, Transport } from '../../utils/httpServer';
import type { Transport } from '../../utils/httpServer'; import type { Page } from '../page';
import { ManualPromise } from '../../utils/manualPromise';
export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp { export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp {
private _recorder: IRecorder; readonly wsEndpointForTest: string | undefined;
private _transport: Transport; private _transport: RecorderTransport;
private _tracePage: Page;
private _traceServer: HttpServer;
static factory(context: BrowserContext): IRecorderAppFactory { static factory(context: BrowserContext): IRecorderAppFactory {
return async (recorder: IRecorder) => { return async (recorder: IRecorder) => {
const transport = new RecorderTransport(); const transport = new RecorderTransport();
const trace = path.join(context._browser.options.tracesDir, 'trace'); const trace = path.join(context._browser.options.tracesDir, 'trace');
await openApp(trace, { transport }); const { wsEndpointForTest, tracePage, traceServer } = await openApp(trace, { transport, headless: !context._browser.options.headful });
return new RecorderInTraceViewer(context, recorder, transport); return new RecorderInTraceViewer(transport, tracePage, traceServer, wsEndpointForTest);
}; };
} }
constructor(context: BrowserContext, recorder: IRecorder, transport: Transport) { constructor(transport: RecorderTransport, tracePage: Page, traceServer: HttpServer, wsEndpointForTest: string | undefined) {
super(); super();
this._recorder = recorder;
this._transport = transport; this._transport = transport;
this._tracePage = tracePage;
this._traceServer = traceServer;
this.wsEndpointForTest = wsEndpointForTest;
this._tracePage.once('close', () => {
this.close();
});
} }
async close(): Promise<void> { async close(): Promise<void> {
this._transport.sendEvent?.('close', {}); await this._tracePage.context().close({ reason: 'Recorder window closed' });
await this._traceServer.stop();
} }
async setPaused(paused: boolean): Promise<void> { async setPaused(paused: boolean): Promise<void> {
this._transport.sendEvent?.('setPaused', { paused }); this._transport.deliverEvent('setPaused', { paused });
} }
async setMode(mode: Mode): Promise<void> { async setMode(mode: Mode): Promise<void> {
this._transport.sendEvent?.('setMode', { mode }); this._transport.deliverEvent('setMode', { mode });
} }
async setFile(file: string): Promise<void> { async setFile(file: string): Promise<void> {
this._transport.sendEvent?.('setFileIfNeeded', { file }); this._transport.deliverEvent('setFileIfNeeded', { file });
} }
async setSelector(selector: string, userGesture?: boolean): Promise<void> { async setSelector(selector: string, userGesture?: boolean): Promise<void> {
this._transport.sendEvent?.('setSelector', { selector, userGesture }); this._transport.deliverEvent('setSelector', { selector, userGesture });
} }
async updateCallLogs(callLogs: CallLog[]): Promise<void> { async updateCallLogs(callLogs: CallLog[]): Promise<void> {
this._transport.sendEvent?.('updateCallLogs', { callLogs }); this._transport.deliverEvent('updateCallLogs', { callLogs });
} }
async setSources(sources: Source[]): Promise<void> { async setSources(sources: Source[]): Promise<void> {
this._transport.sendEvent?.('setSources', { sources }); this._transport.deliverEvent('setSources', { sources });
if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) {
if ((process as any)._didSetSourcesForTest(sources[0].text))
this.close();
}
} }
} }
async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }) { async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }): Promise<{ wsEndpointForTest: string | undefined, tracePage: Page, traceServer: HttpServer }> {
const server = await startTraceViewerServer(options); const traceServer = await startTraceViewerServer(options);
await installRootRedirect(server, [trace], { ...options, webApp: 'recorder.html' }); await installRootRedirect(traceServer, [trace], { ...options, webApp: 'recorder.html' });
const page = await openTraceViewerApp(server.urlPrefix('precise'), 'chromium', options); const page = await openTraceViewerApp(traceServer.urlPrefix('precise'), 'chromium', options);
page.on('close', () => gracefullyProcessExitDoNotHang(0)); return { wsEndpointForTest: page.context()._browser.options.wsEndpoint, tracePage: page, traceServer };
} }
class RecorderTransport implements Transport { class RecorderTransport implements Transport {
private _connected = new ManualPromise<void>();
constructor() { constructor() {
} }
async dispatch(method: string, params: any) { onconnect() {
this._connected.resolve();
}
async dispatch(method: string, params: any): Promise<any> {
} }
onclose() { onclose() {
} }
deliverEvent(method: string, params: any) {
this._connected.then(() => this.sendEvent?.(method, params));
}
sendEvent?: (method: string, params: any) => void; sendEvent?: (method: string, params: any) => void;
close?: () => void; close?: () => void;
} }

View file

@ -20,9 +20,13 @@ import type { Page } from '../page';
import type { ActionInContext } from '../codegen/types'; import type { ActionInContext } from '../codegen/types';
import type { Frame } from '../frames'; import type { Frame } from '../frames';
import type * as actions from './recorderActions'; import type * as actions from './recorderActions';
import { toKeyboardModifiers } from '../codegen/language'; import type * as channels from '@protocol/channels';
import type * as trace from '@trace/trace';
import { fromKeyboardModifiers, toKeyboardModifiers } from '../codegen/language';
import { serializeExpectedTextValues } from '../../utils/expectUtils'; import { serializeExpectedTextValues } from '../../utils/expectUtils';
import { createGuid, monotonicTime } from '../../utils'; import { createGuid, monotonicTime } from '../../utils';
import { parseSerializedValue, serializeValue } from '../../protocol/serializers';
import type { SmartKeyboardModifier } from '../types';
export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog { export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog {
let title = metadata.apiName || metadata.method; let title = metadata.apiName || metadata.method;
@ -76,76 +80,425 @@ export async function frameForAction(pageAliases: Map<Page, string>, actionInCon
return result.frame; return result.frame;
} }
export function traceParamsForAction(actionInContext: ActionInContext) { export function traceParamsForAction(actionInContext: ActionInContext): { method: string, params: any } {
const { action } = actionInContext; const { action } = actionInContext;
switch (action.name) { switch (action.name) {
case 'navigate': return { url: action.url }; case 'navigate': {
case 'openPage': return {}; const params: channels.FrameGotoParams = {
case 'closePage': return {}; url: action.url,
};
return { method: 'goto', params };
}
case 'openPage':
case 'closePage':
throw new Error('Not reached');
} }
const selector = buildFullSelector(actionInContext.frame.framePath, action.selector); const selector = buildFullSelector(actionInContext.frame.framePath, action.selector);
switch (action.name) { switch (action.name) {
case 'click': return { selector, clickCount: action.clickCount }; case 'click': {
case 'press': { const params: channels.FrameClickParams = {
const modifiers = toKeyboardModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+');
return { selector, key: shortcut };
}
case 'fill': return { selector, text: action.text };
case 'setInputFiles': return { selector, files: action.files };
case 'check': return { selector };
case 'uncheck': return { selector };
case 'select': return { selector, values: action.options.map(value => ({ value })) };
case 'assertChecked': {
return {
selector, selector,
strict: true,
modifiers: toKeyboardModifiers(action.modifiers),
button: action.button,
clickCount: action.clickCount,
position: action.position,
};
return { method: 'click', params };
}
case 'press': {
const params: channels.FramePressParams = {
selector,
strict: true,
key: [...toKeyboardModifiers(action.modifiers), action.key].join('+'),
};
return { method: 'press', params };
}
case 'fill': {
const params: channels.FrameFillParams = {
selector,
strict: true,
value: action.text,
};
return { method: 'fill', params };
}
case 'setInputFiles': {
const params: channels.FrameSetInputFilesParams = {
selector,
strict: true,
localPaths: action.files,
};
return { method: 'setInputFiles', params };
}
case 'check': {
const params: channels.FrameCheckParams = {
selector,
strict: true,
};
return { method: 'check', params };
}
case 'uncheck': {
const params: channels.FrameUncheckParams = {
selector,
strict: true,
};
return { method: 'uncheck', params };
}
case 'select': {
const params: channels.FrameSelectOptionParams = {
selector,
strict: true,
options: action.options.map(option => ({ value: option })),
};
return { method: 'selectOption', params };
}
case 'assertChecked': {
const params: channels.FrameExpectParams = {
selector: action.selector,
expression: 'to.be.checked', expression: 'to.be.checked',
isNot: !action.checked, isNot: !action.checked,
}; };
return { method: 'expect', params };
} }
case 'assertText': { case 'assertText': {
return { const params: channels.FrameExpectParams = {
selector, selector,
expression: 'to.have.text', expression: 'to.have.text',
expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }), expectedText: serializeExpectedTextValues([action.text], { matchSubstring: action.substring, normalizeWhiteSpace: true }),
isNot: false, isNot: false,
}; };
return { method: 'expect', params };
} }
case 'assertValue': { case 'assertValue': {
return { const params: channels.FrameExpectParams = {
selector, selector,
expression: 'to.have.value', expression: 'to.have.value',
expectedValue: action.value, expectedValue: { value: serializeValue(action.value, value => ({ fallThrough: value })), handles: [] },
isNot: false, isNot: false,
}; };
return { method: 'expect', params };
} }
case 'assertVisible': { case 'assertVisible': {
return { const params: channels.FrameExpectParams = {
selector, selector,
expression: 'to.be.visible', expression: 'to.be.visible',
isNot: false, isNot: false,
}; };
return { method: 'expect', params };
} }
} }
} }
export function callMetadataForAction(pageAliases: Map<Page, string>, actionInContext: ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } { export function callMetadataForAction(pageAliases: Map<Page, string>, actionInContext: ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } {
const mainFrame = mainFrameForAction(pageAliases, actionInContext); const mainFrame = mainFrameForAction(pageAliases, actionInContext);
const { action } = actionInContext; const { method, params } = traceParamsForAction(actionInContext);
const callMetadata: CallMetadata = { const callMetadata: CallMetadata = {
id: `call@${createGuid()}`, id: `call@${createGuid()}`,
apiName: 'frame.' + action.name, stepId: `recorder@${createGuid()}`,
apiName: 'page.' + method,
objectId: mainFrame.guid, objectId: mainFrame.guid,
pageId: mainFrame._page.guid, pageId: mainFrame._page.guid,
frameId: mainFrame.guid, frameId: mainFrame.guid,
startTime: monotonicTime(), startTime: monotonicTime(),
endTime: 0, endTime: 0,
type: 'Frame', type: 'Frame',
method: action.name, method,
params: traceParamsForAction(actionInContext), params,
log: [], log: [],
}; };
return { callMetadata, mainFrame }; return { callMetadata, mainFrame };
} }
export function traceEventsToAction(events: trace.TraceEvent[]): ActionInContext[] {
const result: ActionInContext[] = [];
const pageAliases = new Map<string, string>();
let lastDownloadOrdinal = 0;
let lastDialogOrdinal = 0;
const addSignal = (signal: actions.Signal) => {
const lastAction = result[result.length - 1];
if (!lastAction)
return;
lastAction.action.signals.push(signal);
};
for (const event of events) {
if (event.type === 'event' && event.class === 'BrowserContext') {
const { method, params } = event;
if (method === 'page') {
const pageAlias = 'page' + (pageAliases.size || '');
pageAliases.set(params.pageId, pageAlias);
addSignal({
name: 'popup',
popupAlias: pageAlias,
});
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'openPage',
url: '',
signals: [],
},
timestamp: event.time,
});
continue;
}
if (method === 'pageClosed') {
const pageAlias = pageAliases.get(event.params.pageId) || 'page';
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'closePage',
signals: [],
},
timestamp: event.time,
});
continue;
}
if (method === 'download') {
const downloadAlias = lastDownloadOrdinal ? String(lastDownloadOrdinal) : '';
++lastDownloadOrdinal;
addSignal({
name: 'download',
downloadAlias,
});
continue;
}
if (method === 'dialog') {
const dialogAlias = lastDialogOrdinal ? String(lastDialogOrdinal) : '';
++lastDialogOrdinal;
addSignal({
name: 'dialog',
dialogAlias,
});
continue;
}
continue;
}
if (event.type !== 'before' || !event.pageId)
continue;
if (!event.stepId?.startsWith('recorder@'))
continue;
const { method, params: untypedParams, pageId } = event;
let pageAlias = pageAliases.get(pageId);
if (!pageAlias) {
pageAlias = 'page';
pageAliases.set(pageId, pageAlias);
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'openPage',
url: '',
signals: [],
},
timestamp: event.startTime,
});
}
if (method === 'goto') {
const params = untypedParams as channels.FrameGotoParams;
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'navigate',
url: params.url,
signals: [],
},
timestamp: event.startTime,
});
continue;
}
if (method === 'click') {
const params = untypedParams as channels.FrameClickParams;
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'click',
selector: params.selector,
signals: [],
button: params.button || 'left',
modifiers: fromKeyboardModifiers(params.modifiers),
clickCount: params.clickCount || 1,
position: params.position,
},
timestamp: event.startTime
});
continue;
}
if (method === 'fill') {
const params = untypedParams as channels.FrameFillParams;
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'fill',
selector: params.selector,
signals: [],
text: params.value,
},
timestamp: event.startTime
});
continue;
}
if (method === 'press') {
const params = untypedParams as channels.FramePressParams;
const tokens = params.key.split('+');
const modifiers = tokens.slice(0, tokens.length - 1) as SmartKeyboardModifier[];
const key = tokens[tokens.length - 1];
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'press',
selector: params.selector,
signals: [],
key,
modifiers: fromKeyboardModifiers(modifiers),
},
timestamp: event.startTime
});
continue;
}
if (method === 'check') {
const params = untypedParams as channels.FrameCheckParams;
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'check',
selector: params.selector,
signals: [],
},
timestamp: event.startTime
});
continue;
}
if (method === 'uncheck') {
const params = untypedParams as channels.FrameUncheckParams;
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'uncheck',
selector: params.selector,
signals: [],
},
timestamp: event.startTime
});
continue;
}
if (method === 'selectOption') {
const params = untypedParams as channels.FrameSelectOptionParams;
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'select',
selector: params.selector,
signals: [],
options: (params.options || []).map(option => option.value!),
},
timestamp: event.startTime
});
continue;
}
if (method === 'setInputFiles') {
const params = untypedParams as channels.FrameSetInputFilesParams;
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'setInputFiles',
selector: params.selector,
signals: [],
files: params.localPaths || [],
},
timestamp: event.startTime
});
continue;
}
if (method === 'expect') {
const params = untypedParams as channels.FrameExpectParams;
if (params.expression === 'to.have.text') {
const entry = params.expectedText?.[0];
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'assertText',
selector: params.selector,
signals: [],
text: entry?.string!,
substring: !!entry?.matchSubstring,
},
timestamp: event.startTime
});
continue;
}
if (params.expression === 'to.have.value') {
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'assertValue',
selector: params.selector,
signals: [],
value: parseSerializedValue(params.expectedValue!.value, params.expectedValue!.handles),
},
timestamp: event.startTime
});
continue;
}
if (params.expression === 'to.be.checked') {
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'assertChecked',
selector: params.selector,
signals: [],
checked: !params.isNot,
},
timestamp: event.startTime
});
continue;
}
if (params.expression === 'to.be.visible') {
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'assertVisible',
selector: params.selector,
signals: [],
},
timestamp: event.startTime
});
continue;
}
continue;
}
}
return result;
}
export function collapseActions(actions: ActionInContext[]): ActionInContext[] {
const result: ActionInContext[] = [];
for (const action of actions) {
const lastAction = result[result.length - 1];
const isSameAction = lastAction && lastAction.action.name === action.action.name && lastAction.frame.pageAlias === action.frame.pageAlias && lastAction.frame.framePath.join('|') === action.frame.framePath.join('|');
const isSameSelector = lastAction && 'selector' in lastAction.action && 'selector' in action.action && action.action.selector === lastAction.action.selector;
const shouldMerge = isSameAction && (action.action.name === 'navigate' || (action.action.name === 'fill' && isSameSelector));
if (!shouldMerge) {
result.push(action);
continue;
}
result[result.length - 1] = action;
}
return result;
}

View file

@ -346,7 +346,7 @@ function rewriteToLocalhostIfNeeded(host: string): string {
} }
export function rewriteOpenSSLErrorIfNeeded(error: Error): Error { export function rewriteOpenSSLErrorIfNeeded(error: Error): Error {
if (error.message !== 'unsupported') if (error.message !== 'unsupported' && (error as NodeJS.ErrnoException).code !== 'ERR_CRYPTO_UNSUPPORTED_OPERATION')
return error; return error;
return rewriteErrorMessage(error, [ return rewriteErrorMessage(error, [
'Unsupported TLS certificate.', 'Unsupported TLS certificate.',

View file

@ -38,6 +38,8 @@ import { Snapshotter } from './snapshotter';
import type { ConsoleMessage } from '../../console'; import type { ConsoleMessage } from '../../console';
import { Dispatcher } from '../../dispatchers/dispatcher'; import { Dispatcher } from '../../dispatchers/dispatcher';
import { serializeError } from '../../errors'; import { serializeError } from '../../errors';
import type { Dialog } from '../../dialog';
import type { Download } from '../../download';
const version: trace.VERSION = 7; const version: trace.VERSION = 7;
@ -79,6 +81,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
private _allResources = new Set<string>(); private _allResources = new Set<string>();
private _contextCreatedEvent: trace.ContextCreatedTraceEvent; private _contextCreatedEvent: trace.ContextCreatedTraceEvent;
private _pendingHarEntries = new Set<har.Entry>(); private _pendingHarEntries = new Set<har.Entry>();
private _inMemoryEvents: trace.TraceEvent[] | undefined;
private _inMemoryEventsCallback: ((events: trace.TraceEvent[]) => void) | undefined;
constructor(context: BrowserContext | APIRequestContext, tracesDir: string | undefined) { constructor(context: BrowserContext | APIRequestContext, tracesDir: string | undefined) {
super(context, 'tracing'); super(context, 'tracing');
@ -179,7 +183,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
wallTime: Date.now(), wallTime: Date.now(),
monotonicTime: monotonicTime() monotonicTime: monotonicTime()
}; };
this._fs.appendFile(this._state.traceFile, JSON.stringify(event) + '\n'); this._appendTraceEvent(event);
this._context.instrumentation.addListener(this, this._context); this._context.instrumentation.addListener(this, this._context);
this._eventListeners.push( this._eventListeners.push(
@ -193,6 +197,11 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
return { traceName: this._state.traceName }; return { traceName: this._state.traceName };
} }
onMemoryEvents(callback: (events: trace.TraceEvent[]) => void) {
this._inMemoryEventsCallback = callback;
this._inMemoryEvents = [];
}
private _startScreencast() { private _startScreencast() {
if (!(this._context instanceof BrowserContext)) if (!(this._context instanceof BrowserContext))
return; return;
@ -447,6 +456,50 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
this._appendTraceEvent(event); this._appendTraceEvent(event);
} }
onDialog(dialog: Dialog) {
const event: trace.EventTraceEvent = {
type: 'event',
time: monotonicTime(),
class: 'BrowserContext',
method: 'dialog',
params: { pageId: dialog.page().guid, type: dialog.type(), message: dialog.message(), defaultValue: dialog.defaultValue() },
};
this._appendTraceEvent(event);
}
onDownload(page: Page, download: Download) {
const event: trace.EventTraceEvent = {
type: 'event',
time: monotonicTime(),
class: 'BrowserContext',
method: 'download',
params: { pageId: page.guid, url: download.url, suggestedFilename: download.suggestedFilename() },
};
this._appendTraceEvent(event);
}
onPageOpen(page: Page) {
const event: trace.EventTraceEvent = {
type: 'event',
time: monotonicTime(),
class: 'BrowserContext',
method: 'page',
params: { pageId: page.guid, openerPageId: page.opener()?.guid },
};
this._appendTraceEvent(event);
}
onPageClose(page: Page) {
const event: trace.EventTraceEvent = {
type: 'event',
time: monotonicTime(),
class: 'BrowserContext',
method: 'pageClosed',
params: { pageId: page.guid },
};
this._appendTraceEvent(event);
}
private _onPageError(error: Error, page: Page) { private _onPageError(error: Error, page: Page) {
const event: trace.EventTraceEvent = { const event: trace.EventTraceEvent = {
type: 'event', type: 'event',
@ -487,6 +540,10 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
// Do not flush (console) events, they are too noisy, unless we are in ui mode (live). // Do not flush (console) events, they are too noisy, unless we are in ui mode (live).
const flush = this._state!.options.live || (event.type !== 'event' && event.type !== 'console' && event.type !== 'log'); const flush = this._state!.options.live || (event.type !== 'event' && event.type !== 'console' && event.type !== 'log');
this._fs.appendFile(this._state!.traceFile, JSON.stringify(visited) + '\n', flush); this._fs.appendFile(this._state!.traceFile, JSON.stringify(visited) + '\n', flush);
if (this._inMemoryEvents) {
this._inMemoryEvents.push(event);
this._inMemoryEventsCallback?.(this._inMemoryEvents);
}
} }
private _appendResource(sha1: string, buffer: Buffer) { private _appendResource(sha1: string, buffer: Buffer) {

View file

@ -223,6 +223,9 @@ class StdinServer implements Transport {
process.stdin.on('close', () => gracefullyProcessExitDoNotHang(0)); process.stdin.on('close', () => gracefullyProcessExitDoNotHang(0));
} }
onconnect() {
}
async dispatch(method: string, params: any) { async dispatch(method: string, params: any) {
if (method === 'initialize') { if (method === 'initialize') {
if (this._traceUrl) if (this._traceUrl)

View file

@ -27,8 +27,9 @@ export type ServerRouteHandler = (request: http.IncomingMessage, response: http.
export type Transport = { export type Transport = {
sendEvent?: (method: string, params: any) => void; sendEvent?: (method: string, params: any) => void;
dispatch: (method: string, params: any) => Promise<any>;
close?: () => void; close?: () => void;
onconnect: () => void;
dispatch: (method: string, params: any) => Promise<any>;
onclose: () => void; onclose: () => void;
}; };
@ -82,6 +83,7 @@ export class HttpServer {
this._wsGuid = guid || createGuid(); this._wsGuid = guid || createGuid();
const wss = new wsServer({ server: this._server, path: '/' + this._wsGuid }); const wss = new wsServer({ server: this._server, path: '/' + this._wsGuid });
wss.on('connection', ws => { wss.on('connection', ws => {
transport.onconnect();
transport.sendEvent = (method, params) => ws.send(JSON.stringify({ method, params })); transport.sendEvent = (method, params) => ws.send(JSON.stringify({ method, params }));
transport.close = () => ws.close(); transport.close = () => ws.close();
ws.on('message', async message => { ws.on('message', async message => {

View file

@ -50,16 +50,6 @@ export function asLocators(lang: Language, selector: string, isFrameLocator: boo
function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFrameLocator: boolean = false, maxOutputSize = 20): string[] { function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFrameLocator: boolean = false, maxOutputSize = 20): string[] {
const parts = [...parsed.parts]; const parts = [...parsed.parts];
// frameLocator('iframe').first is actually "iframe >> nth=0 >> internal:control=enter-frame"
// To make it easier to parse, we turn it into "iframe >> internal:control=enter-frame >> nth=0"
for (let index = 0; index < parts.length - 1; index++) {
if (parts[index].name === 'nth' && parts[index + 1].name === 'internal:control' && (parts[index + 1].body as string) === 'enter-frame') {
// Swap nth and enter-frame.
const [nth] = parts.splice(index, 1);
parts.splice(index + 1, 0, nth);
}
}
const tokens: string[][] = []; const tokens: string[][] = [];
let nextBase: LocatorBase = isFrameLocator ? 'frame-locator' : 'page'; let nextBase: LocatorBase = isFrameLocator ? 'frame-locator' : 'page';
for (let index = 0; index < parts.length; index++) { for (let index = 0; index < parts.length; index++) {
@ -167,15 +157,15 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
continue; continue;
} }
} }
if (part.name === 'internal:control' && (part.body as string) === 'enter-frame') {
tokens.push([factory.generateLocator(base, 'frame', '')]);
nextBase = 'frame-locator';
continue;
}
let locatorType: LocatorType = 'default'; let locatorType: LocatorType = 'default';
const nextPart = parts[index + 1]; const nextPart = parts[index + 1];
if (nextPart && nextPart.name === 'internal:control' && (nextPart.body as string) === 'enter-frame') {
locatorType = 'frame';
nextBase = 'frame-locator';
index++;
}
const selectorPart = stringifySelector({ parts: [part] }); const selectorPart = stringifySelector({ parts: [part] });
const locatorPart = factory.generateLocator(base, locatorType, selectorPart); const locatorPart = factory.generateLocator(base, locatorType, selectorPart);
@ -264,7 +254,7 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
return `locator(${this.quote(body as string)}, { hasNotText: ${this.toHasText(options.hasNotText)} })`; return `locator(${this.quote(body as string)}, { hasNotText: ${this.toHasText(options.hasNotText)} })`;
return `locator(${this.quote(body as string)})`; return `locator(${this.quote(body as string)})`;
case 'frame': case 'frame':
return `frameLocator(${this.quote(body as string)})`; return `contentFrame()`;
case 'nth': case 'nth':
return `nth(${body})`; return `nth(${body})`;
case 'first': case 'first':
@ -356,7 +346,7 @@ export class PythonLocatorFactory implements LocatorFactory {
return `locator(${this.quote(body as string)}, has_not_text=${this.toHasText(options.hasNotText)})`; return `locator(${this.quote(body as string)}, has_not_text=${this.toHasText(options.hasNotText)})`;
return `locator(${this.quote(body as string)})`; return `locator(${this.quote(body as string)})`;
case 'frame': case 'frame':
return `frame_locator(${this.quote(body as string)})`; return `content_frame`;
case 'nth': case 'nth':
return `nth(${body})`; return `nth(${body})`;
case 'first': case 'first':
@ -461,7 +451,7 @@ export class JavaLocatorFactory implements LocatorFactory {
return `locator(${this.quote(body as string)}, new ${clazz}.LocatorOptions().setHasNotText(${this.toHasText(options.hasNotText)}))`; return `locator(${this.quote(body as string)}, new ${clazz}.LocatorOptions().setHasNotText(${this.toHasText(options.hasNotText)}))`;
return `locator(${this.quote(body as string)})`; return `locator(${this.quote(body as string)})`;
case 'frame': case 'frame':
return `frameLocator(${this.quote(body as string)})`; return `contentFrame()`;
case 'nth': case 'nth':
return `nth(${body})`; return `nth(${body})`;
case 'first': case 'first':
@ -556,7 +546,7 @@ export class CSharpLocatorFactory implements LocatorFactory {
return `Locator(${this.quote(body as string)}, new() { ${this.toHasNotText(options.hasNotText)} })`; return `Locator(${this.quote(body as string)}, new() { ${this.toHasNotText(options.hasNotText)} })`;
return `Locator(${this.quote(body as string)})`; return `Locator(${this.quote(body as string)})`;
case 'frame': case 'frame':
return `FrameLocator(${this.quote(body as string)})`; return `ContentFrame`;
case 'nth': case 'nth':
return `Nth(${body})`; return `Nth(${body})`;
case 'first': case 'first':

View file

@ -75,6 +75,7 @@ function parseLocator(locator: string, testIdAttributeName: string): { selector:
.replace(/has_text/g, 'hastext') .replace(/has_text/g, 'hastext')
.replace(/has_not/g, 'hasnot') .replace(/has_not/g, 'hasnot')
.replace(/frame_locator/g, 'framelocator') .replace(/frame_locator/g, 'framelocator')
.replace(/content_frame/g, 'contentframe')
.replace(/[{}\s]/g, '') .replace(/[{}\s]/g, '')
.replace(/new\(\)/g, '') .replace(/new\(\)/g, '')
.replace(/new[\w]+\.[\w]+options\(\)/g, '') .replace(/new[\w]+\.[\w]+options\(\)/g, '')
@ -154,6 +155,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
template = template template = template
.replace(/\,set([\w]+)\(([^)]+)\)/g, (_, group1, group2) => ',' + group1.toLowerCase() + '=' + group2.toLowerCase()) .replace(/\,set([\w]+)\(([^)]+)\)/g, (_, group1, group2) => ',' + group1.toLowerCase() + '=' + group2.toLowerCase())
.replace(/framelocator\(([^)]+)\)/g, '$1.internal:control=enter-frame') .replace(/framelocator\(([^)]+)\)/g, '$1.internal:control=enter-frame')
.replace(/contentframe(\(\))?/g, 'internal:control=enter-frame')
.replace(/locator\(([^)]+),hastext=([^),]+)\)/g, 'locator($1).internal:has-text=$2') .replace(/locator\(([^)]+),hastext=([^),]+)\)/g, 'locator($1).internal:has-text=$2')
.replace(/locator\(([^)]+),hasnottext=([^),]+)\)/g, 'locator($1).internal:has-not-text=$2') .replace(/locator\(([^)]+),hasnottext=([^),]+)\)/g, 'locator($1).internal:has-not-text=$2')
.replace(/locator\(([^)]+),hastext=([^),]+)\)/g, 'locator($1).internal:has-text=$2') .replace(/locator\(([^)]+),hastext=([^),]+)\)/g, 'locator($1).internal:has-text=$2')

View file

@ -840,7 +840,7 @@ CORS RFC1918 enforcement.
resourceIPAddressSpace?: Network.IPAddressSpace; resourceIPAddressSpace?: Network.IPAddressSpace;
clientSecurityState?: Network.ClientSecurityState; clientSecurityState?: Network.ClientSecurityState;
} }
export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader"; export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader"|"NavigationRegistrationUniqueScopeAlreadySet";
export type SharedDictionaryError = "UseErrorCrossOriginNoCorsRequest"|"UseErrorDictionaryLoadFailure"|"UseErrorMatchingDictionaryNotUsed"|"UseErrorUnexpectedContentDictionaryHeader"|"WriteErrorCossOriginNoCorsRequest"|"WriteErrorDisallowedBySettings"|"WriteErrorExpiredResponse"|"WriteErrorFeatureDisabled"|"WriteErrorInsufficientResources"|"WriteErrorInvalidMatchField"|"WriteErrorInvalidStructuredHeader"|"WriteErrorNavigationRequest"|"WriteErrorNoMatchField"|"WriteErrorNonListMatchDestField"|"WriteErrorNonSecureContext"|"WriteErrorNonStringIdField"|"WriteErrorNonStringInMatchDestList"|"WriteErrorNonStringMatchField"|"WriteErrorNonTokenTypeField"|"WriteErrorRequestAborted"|"WriteErrorShuttingDown"|"WriteErrorTooLongIdField"|"WriteErrorUnsupportedType"; export type SharedDictionaryError = "UseErrorCrossOriginNoCorsRequest"|"UseErrorDictionaryLoadFailure"|"UseErrorMatchingDictionaryNotUsed"|"UseErrorUnexpectedContentDictionaryHeader"|"WriteErrorCossOriginNoCorsRequest"|"WriteErrorDisallowedBySettings"|"WriteErrorExpiredResponse"|"WriteErrorFeatureDisabled"|"WriteErrorInsufficientResources"|"WriteErrorInvalidMatchField"|"WriteErrorInvalidStructuredHeader"|"WriteErrorNavigationRequest"|"WriteErrorNoMatchField"|"WriteErrorNonListMatchDestField"|"WriteErrorNonSecureContext"|"WriteErrorNonStringIdField"|"WriteErrorNonStringInMatchDestList"|"WriteErrorNonStringMatchField"|"WriteErrorNonTokenTypeField"|"WriteErrorRequestAborted"|"WriteErrorShuttingDown"|"WriteErrorTooLongIdField"|"WriteErrorUnsupportedType";
/** /**
* Details for issues around "Attribution Reporting API" usage. * Details for issues around "Attribution Reporting API" usage.
@ -1534,7 +1534,7 @@ events afterwards if enabled and recording.
*/ */
windowState?: WindowState; windowState?: WindowState;
} }
export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"windowManagement"; export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"windowManagement";
export type PermissionSetting = "granted"|"denied"|"prompt"; export type PermissionSetting = "granted"|"denied"|"prompt";
/** /**
* Definition of PermissionDescriptor defined in the Permissions API: * Definition of PermissionDescriptor defined in the Permissions API:
@ -3561,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"|"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"; export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"column"|"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"|"placeholder"|"file-selector-button"|"details-content"|"select-fallback-button"|"select-fallback-button-text"|"picker";
/** /**
* Shadow root type. * Shadow root type.
*/ */
@ -3710,6 +3710,7 @@ The property is always undefined now.
isSVG?: boolean; isSVG?: boolean;
compatibilityMode?: CompatibilityMode; compatibilityMode?: CompatibilityMode;
assignedSlot?: BackendNode; assignedSlot?: BackendNode;
isScrollable?: boolean;
} }
/** /**
* A structure to hold the top-level node of a detached tree and an array of its retained descendants. * A structure to hold the top-level node of a detached tree and an array of its retained descendants.
@ -3954,6 +3955,19 @@ The property is always undefined now.
* Called when top layer elements are changed. * Called when top layer elements are changed.
*/ */
export type topLayerElementsUpdatedPayload = void; export type topLayerElementsUpdatedPayload = void;
/**
* Fired when a node's scrollability state changes.
*/
export type scrollableFlagUpdatedPayload = {
/**
* The id of the node.
*/
nodeId: DOM.NodeId;
/**
* If the node is scrollable.
*/
isScrollable: boolean;
}
/** /**
* Called when a pseudo element is removed from an element. * Called when a pseudo element is removed from an element.
*/ */
@ -8102,8 +8116,25 @@ or hexadecimal (0x prefixed) string.
*/ */
size: number; size: number;
} }
/**
* DOM object counter data.
*/
export interface DOMCounter {
/**
* Object name. Note: object names should be presumed volatile and clients should not expect
the returned names to be consistent across runs.
*/
name: string;
/**
* Object count.
*/
count: number;
}
/**
* Retruns current DOM object counters.
*/
export type getDOMCountersParameters = { export type getDOMCountersParameters = {
} }
export type getDOMCountersReturnValue = { export type getDOMCountersReturnValue = {
@ -8111,6 +8142,21 @@ or hexadecimal (0x prefixed) string.
nodes: number; nodes: number;
jsEventListeners: number; jsEventListeners: number;
} }
/**
* Retruns DOM object counters after preparing renderer for leak detection.
*/
export type getDOMCountersForLeakDetectionParameters = {
}
export type getDOMCountersForLeakDetectionReturnValue = {
/**
* DOM object counters.
*/
counters: DOMCounter[];
}
/**
* Prepares for leak detection by terminating workers, stopping spellcheckers,
dropping non-essential internal caches, running garbage collections, etc.
*/
export type prepareForLeakDetectionParameters = { export type prepareForLeakDetectionParameters = {
} }
export type prepareForLeakDetectionReturnValue = { export type prepareForLeakDetectionReturnValue = {
@ -8902,7 +8948,7 @@ This is a temporary ability and it will be removed in the future.
/** /**
* Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request. * Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request.
*/ */
export type CookieExemptionReason = "None"|"UserSetting"|"TPCDMetadata"|"TPCDDeprecationTrial"|"TPCDHeuristics"|"EnterprisePolicy"|"StorageAccess"|"TopLevelStorageAccess"|"CorsOptIn"|"Scheme"; export type CookieExemptionReason = "None"|"UserSetting"|"TPCDMetadata"|"TPCDDeprecationTrial"|"TopLevelTPCDDeprecationTrial"|"TPCDHeuristics"|"EnterprisePolicy"|"StorageAccess"|"TopLevelStorageAccess"|"Scheme";
/** /**
* A cookie which was not stored from a response with the corresponding reason. * A cookie which was not stored from a response with the corresponding reason.
*/ */
@ -11452,7 +11498,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"|"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"; 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"|"controlled-frame"|"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"|"popins"|"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-app-installation"|"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.
*/ */
@ -12040,7 +12086,7 @@ https://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-expl
/** /**
* List of not restored reasons for back-forward cache. * List of not restored reasons for back-forward cache.
*/ */
export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient"; export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"ContentDiscarded"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient";
/** /**
* Types of not restored reasons for back-forward cache. * Types of not restored reasons for back-forward cache.
*/ */
@ -12151,6 +12197,16 @@ dependent on the reason:
frameId: FrameId; frameId: FrameId;
reason: "remove"|"swap"; reason: "remove"|"swap";
} }
/**
* Fired before frame subtree is detached. Emitted before any frame of the
subtree is actually detached.
*/
export type frameSubtreeWillBeDetachedPayload = {
/**
* Id of the frame that is the root of the subtree that will be detached.
*/
frameId: FrameId;
}
/** /**
* Fired once navigation of the frame has completed. Frame is now associated with the new loader. * Fired once navigation of the frame has completed. Frame is now associated with the new loader.
*/ */
@ -14250,6 +14306,15 @@ int, only present for source registrations
debugData: AttributionReportingAggregatableDebugReportingData[]; debugData: AttributionReportingAggregatableDebugReportingData[];
aggregationCoordinatorOrigin?: string; aggregationCoordinatorOrigin?: string;
} }
export interface AttributionScopesData {
values: string[];
/**
* number instead of integer because not all uint32 can be represented by
int
*/
limit: number;
maxEventStates: number;
}
export interface AttributionReportingSourceRegistration { export interface AttributionReportingSourceRegistration {
time: Network.TimeSinceEpoch; time: Network.TimeSinceEpoch;
/** /**
@ -14273,8 +14338,9 @@ int, only present for source registrations
triggerDataMatching: AttributionReportingTriggerDataMatching; triggerDataMatching: AttributionReportingTriggerDataMatching;
destinationLimitPriority: SignedInt64AsBase10; destinationLimitPriority: SignedInt64AsBase10;
aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig; aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig;
scopesData?: AttributionScopesData;
} }
export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"destinationPerDayReportingLimitReached"; export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxScopesChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"exceedsMaxEventStatesLimit"|"destinationPerDayReportingLimitReached";
export type AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude"; export type AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude";
export interface AttributionReportingAggregatableValueDictEntry { export interface AttributionReportingAggregatableValueDictEntry {
key: string; key: string;
@ -14317,6 +14383,7 @@ int
sourceRegistrationTimeConfig: AttributionReportingSourceRegistrationTimeConfig; sourceRegistrationTimeConfig: AttributionReportingSourceRegistrationTimeConfig;
triggerContextId?: string; triggerContextId?: string;
aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig; aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig;
scopes: string[];
} }
export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData"; export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData";
export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports"; export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports";
@ -17019,7 +17086,7 @@ status is shared by prefetchStatusUpdated and prerenderStatusUpdated.
* TODO(https://crbug.com/1384419): revisit the list of PrefetchStatus and * TODO(https://crbug.com/1384419): revisit the list of PrefetchStatus and
filter out the ones that aren't necessary to the developers. filter out the ones that aren't necessary to the developers.
*/ */
export type PrefetchStatus = "PrefetchAllowed"|"PrefetchFailedIneligibleRedirect"|"PrefetchFailedInvalidRedirect"|"PrefetchFailedMIMENotSupported"|"PrefetchFailedNetError"|"PrefetchFailedNon2XX"|"PrefetchFailedPerPageLimitExceeded"|"PrefetchEvictedAfterCandidateRemoved"|"PrefetchEvictedForNewerPrefetch"|"PrefetchHeldback"|"PrefetchIneligibleRetryAfter"|"PrefetchIsPrivacyDecoy"|"PrefetchIsStale"|"PrefetchNotEligibleBrowserContextOffTheRecord"|"PrefetchNotEligibleDataSaverEnabled"|"PrefetchNotEligibleExistingProxy"|"PrefetchNotEligibleHostIsNonUnique"|"PrefetchNotEligibleNonDefaultStoragePartition"|"PrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy"|"PrefetchNotEligibleSchemeIsNotHttps"|"PrefetchNotEligibleUserHasCookies"|"PrefetchNotEligibleUserHasServiceWorker"|"PrefetchNotEligibleBatterySaverEnabled"|"PrefetchNotEligiblePreloadingDisabled"|"PrefetchNotFinishedInTime"|"PrefetchNotStarted"|"PrefetchNotUsedCookiesChanged"|"PrefetchProxyNotAvailable"|"PrefetchResponseUsed"|"PrefetchSuccessfulButNotUsed"|"PrefetchNotUsedProbeFailed"; export type PrefetchStatus = "PrefetchAllowed"|"PrefetchFailedIneligibleRedirect"|"PrefetchFailedInvalidRedirect"|"PrefetchFailedMIMENotSupported"|"PrefetchFailedNetError"|"PrefetchFailedNon2XX"|"PrefetchEvictedAfterCandidateRemoved"|"PrefetchEvictedForNewerPrefetch"|"PrefetchHeldback"|"PrefetchIneligibleRetryAfter"|"PrefetchIsPrivacyDecoy"|"PrefetchIsStale"|"PrefetchNotEligibleBrowserContextOffTheRecord"|"PrefetchNotEligibleDataSaverEnabled"|"PrefetchNotEligibleExistingProxy"|"PrefetchNotEligibleHostIsNonUnique"|"PrefetchNotEligibleNonDefaultStoragePartition"|"PrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy"|"PrefetchNotEligibleSchemeIsNotHttps"|"PrefetchNotEligibleUserHasCookies"|"PrefetchNotEligibleUserHasServiceWorker"|"PrefetchNotEligibleBatterySaverEnabled"|"PrefetchNotEligiblePreloadingDisabled"|"PrefetchNotFinishedInTime"|"PrefetchNotStarted"|"PrefetchNotUsedCookiesChanged"|"PrefetchProxyNotAvailable"|"PrefetchResponseUsed"|"PrefetchSuccessfulButNotUsed"|"PrefetchNotUsedProbeFailed";
/** /**
* Information of headers to be displayed when the header mismatch occurred. * Information of headers to be displayed when the header mismatch occurred.
*/ */
@ -20115,6 +20182,7 @@ Error was thrown.
"DOM.inlineStyleInvalidated": DOM.inlineStyleInvalidatedPayload; "DOM.inlineStyleInvalidated": DOM.inlineStyleInvalidatedPayload;
"DOM.pseudoElementAdded": DOM.pseudoElementAddedPayload; "DOM.pseudoElementAdded": DOM.pseudoElementAddedPayload;
"DOM.topLayerElementsUpdated": DOM.topLayerElementsUpdatedPayload; "DOM.topLayerElementsUpdated": DOM.topLayerElementsUpdatedPayload;
"DOM.scrollableFlagUpdated": DOM.scrollableFlagUpdatedPayload;
"DOM.pseudoElementRemoved": DOM.pseudoElementRemovedPayload; "DOM.pseudoElementRemoved": DOM.pseudoElementRemovedPayload;
"DOM.setChildNodes": DOM.setChildNodesPayload; "DOM.setChildNodes": DOM.setChildNodesPayload;
"DOM.shadowRootPopped": DOM.shadowRootPoppedPayload; "DOM.shadowRootPopped": DOM.shadowRootPoppedPayload;
@ -20173,6 +20241,7 @@ Error was thrown.
"Page.frameAttached": Page.frameAttachedPayload; "Page.frameAttached": Page.frameAttachedPayload;
"Page.frameClearedScheduledNavigation": Page.frameClearedScheduledNavigationPayload; "Page.frameClearedScheduledNavigation": Page.frameClearedScheduledNavigationPayload;
"Page.frameDetached": Page.frameDetachedPayload; "Page.frameDetached": Page.frameDetachedPayload;
"Page.frameSubtreeWillBeDetached": Page.frameSubtreeWillBeDetachedPayload;
"Page.frameNavigated": Page.frameNavigatedPayload; "Page.frameNavigated": Page.frameNavigatedPayload;
"Page.documentOpened": Page.documentOpenedPayload; "Page.documentOpened": Page.documentOpenedPayload;
"Page.frameResized": Page.frameResizedPayload; "Page.frameResized": Page.frameResizedPayload;
@ -20539,6 +20608,7 @@ Error was thrown.
"Log.startViolationsReport": Log.startViolationsReportParameters; "Log.startViolationsReport": Log.startViolationsReportParameters;
"Log.stopViolationsReport": Log.stopViolationsReportParameters; "Log.stopViolationsReport": Log.stopViolationsReportParameters;
"Memory.getDOMCounters": Memory.getDOMCountersParameters; "Memory.getDOMCounters": Memory.getDOMCountersParameters;
"Memory.getDOMCountersForLeakDetection": Memory.getDOMCountersForLeakDetectionParameters;
"Memory.prepareForLeakDetection": Memory.prepareForLeakDetectionParameters; "Memory.prepareForLeakDetection": Memory.prepareForLeakDetectionParameters;
"Memory.forciblyPurgeJavaScriptMemory": Memory.forciblyPurgeJavaScriptMemoryParameters; "Memory.forciblyPurgeJavaScriptMemory": Memory.forciblyPurgeJavaScriptMemoryParameters;
"Memory.setPressureNotificationsSuppressed": Memory.setPressureNotificationsSuppressedParameters; "Memory.setPressureNotificationsSuppressed": Memory.setPressureNotificationsSuppressedParameters;
@ -21148,6 +21218,7 @@ Error was thrown.
"Log.startViolationsReport": Log.startViolationsReportReturnValue; "Log.startViolationsReport": Log.startViolationsReportReturnValue;
"Log.stopViolationsReport": Log.stopViolationsReportReturnValue; "Log.stopViolationsReport": Log.stopViolationsReportReturnValue;
"Memory.getDOMCounters": Memory.getDOMCountersReturnValue; "Memory.getDOMCounters": Memory.getDOMCountersReturnValue;
"Memory.getDOMCountersForLeakDetection": Memory.getDOMCountersForLeakDetectionReturnValue;
"Memory.prepareForLeakDetection": Memory.prepareForLeakDetectionReturnValue; "Memory.prepareForLeakDetection": Memory.prepareForLeakDetectionReturnValue;
"Memory.forciblyPurgeJavaScriptMemory": Memory.forciblyPurgeJavaScriptMemoryReturnValue; "Memory.forciblyPurgeJavaScriptMemory": Memory.forciblyPurgeJavaScriptMemoryReturnValue;
"Memory.setPressureNotificationsSuppressed": Memory.setPressureNotificationsSuppressedReturnValue; "Memory.setPressureNotificationsSuppressed": Memory.setPressureNotificationsSuppressedReturnValue;

View file

@ -3826,6 +3826,34 @@ export interface Page {
url?: string|RegExp; url?: string|RegExp;
}): Promise<void>; }): Promise<void>;
/**
* This method allows to modify websocket connections that are made by the page.
*
* Note that only `WebSocket`s created after this method was called will be routed. It is recommended to call this
* method before navigating the page.
*
* **Usage**
*
* Below is an example of a simple handler that blocks some websocket messages. See {@link WebSocketRoute} for more
* details and examples.
*
* ```js
* await page.routeWebSocket('/ws', async ws => {
* ws.routeSend(message => {
* if (message === 'to-be-blocked')
* return;
* ws.send(message);
* });
* await ws.connect();
* });
* ```
*
* @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the
* `baseURL` from the context options.
* @param handler Handler function to route the WebSocket.
*/
routeWebSocket(url: string|RegExp|((url: URL) => boolean), handler: ((websocketroute: WebSocketRoute) => Promise<any>|any)): Promise<void>;
/** /**
* Returns the buffer with the captured screenshot. * Returns the buffer with the captured screenshot.
* @param options * @param options
@ -8658,6 +8686,34 @@ export interface BrowserContext {
url?: string|RegExp; url?: string|RegExp;
}): Promise<void>; }): Promise<void>;
/**
* This method allows to modify websocket connections that are made by any page in the browser context.
*
* Note that only `WebSocket`s created after this method was called will be routed. It is recommended to call this
* method before creating any pages.
*
* **Usage**
*
* Below is an example of a simple handler that blocks some websocket messages. See {@link WebSocketRoute} for more
* details and examples.
*
* ```js
* await context.routeWebSocket('/ws', async ws => {
* ws.routeSend(message => {
* if (message === 'to-be-blocked')
* return;
* ws.send(message);
* });
* await ws.connect();
* });
* ```
*
* @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the
* `baseURL` from the context options.
* @param handler Handler function to route the WebSocket.
*/
routeWebSocket(url: string|RegExp|((url: URL) => boolean), handler: ((websocketroute: WebSocketRoute) => Promise<any>|any)): Promise<void>;
/** /**
* **NOTE** Service workers are only supported on Chromium-based browsers. * **NOTE** Service workers are only supported on Chromium-based browsers.
* *
@ -14567,6 +14623,134 @@ export interface CDPSession {
detach(): Promise<void>; detach(): Promise<void>;
} }
/**
* Whenever a [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) route is set up with
* [page.routeWebSocket(url, handler)](https://playwright.dev/docs/api/class-page#page-route-web-socket) or
* [browserContext.routeWebSocket(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browser-context-route-web-socket),
* the `WebSocketRoute` object allows to handle the WebSocket.
*
* By default, the routed WebSocket will not actually connect to the server. This way, you can mock entire
* communcation over the WebSocket. Here is an example that responds to a `"query"` with a `"result"`.
*
* ```js
* await page.routeWebSocket('/ws', async ws => {
* ws.routeSend(message => {
* if (message === 'query')
* ws.receive('result');
* });
* });
* ```
*
*/
export interface WebSocketRoute {
/**
* This method allows to route messages that are sent by `WebSocket.send()` call in the page, instead of actually
* sending them to the server. Once this method is called, sent messages **are not** automatically forwarded to the
* server - you should do that manually by calling
* [webSocketRoute.send(message)](https://playwright.dev/docs/api/class-websocketroute#web-socket-route-send).
*
* Calling this method again times will override the handler with a new one.
* @param handler Handler function to route sent messages.
*/
routeSend(handler: (message: string | Buffer) => any): void;
/**
* This method allows to route messages that are received by the
* [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page from the server. This
* method only makes sense if you are also calling
* [webSocketRoute.connect()](https://playwright.dev/docs/api/class-websocketroute#web-socket-route-connect).
*
* Once this method is called, received messages are not automatically dispatched to the
* [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page - you should do that
* manually by calling
* [webSocketRoute.receive(message)](https://playwright.dev/docs/api/class-websocketroute#web-socket-route-receive).
*
* Calling this method again times will override the handler with a new one.
* @param handler Handler function to route received messages.
*/
routeReceive(handler: (message: string | Buffer) => any): void;
/**
* Emitted when the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) closes.
*/
on(event: 'close', listener: () => any): this;
/**
* Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.
*/
once(event: 'close', listener: () => any): this;
/**
* Emitted when the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) closes.
*/
addListener(event: 'close', listener: () => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
removeListener(event: 'close', listener: () => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
off(event: 'close', listener: () => any): this;
/**
* Emitted when the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) closes.
*/
prependListener(event: 'close', listener: () => any): this;
/**
* Closes the server connection and the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
* object in the page.
* @param options
*/
close(options?: {
/**
* Optional [close code](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#code).
*/
code?: number;
/**
* Optional [close reason](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#reason).
*/
reason?: string;
}): Promise<void>;
/**
* By default, routed WebSocket does not connect to the server, so you can mock entire WebSocket communication. This
* method connects to the actual WebSocket server, giving the ability to send and receive messages from the server.
*
* Once connected:
* - Messages received from the server will be automatically dispatched to the
* [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page, unless
* [webSocketRoute.routeReceive(handler)](https://playwright.dev/docs/api/class-websocketroute#web-socket-route-route-receive)
* is called.
* - Messages sent by the `WebSocket.send()` call in the page will be automatically sent to the server, unless
* [webSocketRoute.routeSend(handler)](https://playwright.dev/docs/api/class-websocketroute#web-socket-route-route-send)
* is called.
*/
connect(): Promise<void>;
/**
* Dispatches a message to the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the
* page, like it was received from the server.
* @param message Message to receive.
*/
receive(message: string|Buffer): void;
/**
* Sends a message to the server, like it was sent in the page with `WebSocket.send()`.
* @param message Message to send.
*/
send(message: string|Buffer): void;
/**
* URL of the WebSocket created in the page.
*/
url(): string;
[Symbol.asyncDispose](): Promise<void>;
}
type DeviceDescriptor = { type DeviceDescriptor = {
viewport: ViewportSize; viewport: ViewportSize;
userAgent: string; userAgent: string;
@ -18240,11 +18424,12 @@ export interface FileChooser {
/** /**
* FrameLocator represents a view to the `iframe` on the page. It captures the logic sufficient to retrieve the * FrameLocator represents a view to the `iframe` on the page. It captures the logic sufficient to retrieve the
* `iframe` and locate elements in that iframe. FrameLocator can be created with either * `iframe` and locate elements in that iframe. FrameLocator can be created with either
* [locator.contentFrame()](https://playwright.dev/docs/api/class-locator#locator-content-frame),
* [page.frameLocator(selector)](https://playwright.dev/docs/api/class-page#page-frame-locator) or * [page.frameLocator(selector)](https://playwright.dev/docs/api/class-page#page-frame-locator) or
* [locator.frameLocator(selector)](https://playwright.dev/docs/api/class-locator#locator-frame-locator) method. * [locator.frameLocator(selector)](https://playwright.dev/docs/api/class-locator#locator-frame-locator) method.
* *
* ```js * ```js
* const locator = page.frameLocator('#my-frame').getByText('Submit'); * const locator = page.locator('#my-frame').contentFrame().getByText('Submit');
* await locator.click(); * await locator.click();
* ``` * ```
* *
@ -18255,10 +18440,10 @@ export interface FileChooser {
* *
* ```js * ```js
* // Throws if there are several frames in DOM: * // Throws if there are several frames in DOM:
* await page.frameLocator('.result-frame').getByRole('button').click(); * await page.locator('.result-frame').contentFrame().getByRole('button').click();
* *
* // Works because we explicitly tell locator to pick the first frame: * // Works because we explicitly tell locator to pick the first frame:
* await page.frameLocator('.result-frame').first().getByRole('button').click(); * await page.locator('.result-frame').contentFrame().first().getByRole('button').click();
* ``` * ```
* *
* **Converting Locator to FrameLocator** * **Converting Locator to FrameLocator**
@ -18274,6 +18459,8 @@ export interface FileChooser {
export interface FrameLocator { export interface FrameLocator {
/** /**
* Returns locator to the first matching frame. * Returns locator to the first matching frame.
* @deprecated Use [locator.first()](https://playwright.dev/docs/api/class-locator#locator-first) followed by
* [locator.contentFrame()](https://playwright.dev/docs/api/class-locator#locator-content-frame) instead.
*/ */
first(): FrameLocator; first(): FrameLocator;
@ -18598,6 +18785,8 @@ export interface FrameLocator {
/** /**
* Returns locator to the last matching frame. * Returns locator to the last matching frame.
* @deprecated Use [locator.last()](https://playwright.dev/docs/api/class-locator#locator-last) followed by
* [locator.contentFrame()](https://playwright.dev/docs/api/class-locator#locator-content-frame) instead.
*/ */
last(): FrameLocator; last(): FrameLocator;
@ -18650,6 +18839,8 @@ export interface FrameLocator {
/** /**
* Returns locator to the n-th matching frame. It's zero based, `nth(0)` selects the first frame. * Returns locator to the n-th matching frame. It's zero based, `nth(0)` selects the first frame.
* @deprecated Use [locator.nth(index)](https://playwright.dev/docs/api/class-locator#locator-nth) followed by
* [locator.contentFrame()](https://playwright.dev/docs/api/class-locator#locator-content-frame) instead.
* @param index * @param index
*/ */
nth(index: number): FrameLocator; nth(index: number): FrameLocator;
@ -18666,7 +18857,7 @@ export interface FrameLocator {
* **Usage** * **Usage**
* *
* ```js * ```js
* const frameLocator = page.frameLocator('iframe[name="embedded"]'); * const frameLocator = page.locator('iframe[name="embedded"]').contentFrame();
* // ... * // ...
* const locator = frameLocator.owner(); * const locator = frameLocator.owner();
* await expect(locator).toBeVisible(); * await expect(locator).toBeVisible();

View file

@ -45,6 +45,7 @@ function babelTransformOptions(isTypeScript: boolean, isModule: boolean, plugins
[require('@babel/plugin-syntax-async-generators')], [require('@babel/plugin-syntax-async-generators')],
[require('@babel/plugin-syntax-object-rest-spread')], [require('@babel/plugin-syntax-object-rest-spread')],
[require('@babel/plugin-transform-export-namespace-from')], [require('@babel/plugin-transform-export-namespace-from')],
[require('@babel/plugin-syntax-import-attributes'), { deprecatedAssertSyntax: true }],
[ [
// From https://github.com/G-Rath/babel-plugin-replace-ts-export-assignment/blob/8dfdca32c8aa428574b0cae341444fc5822f2dc6/src/index.ts // From https://github.com/G-Rath/babel-plugin-replace-ts-export-assignment/blob/8dfdca32c8aa428574b0cae341444fc5822f2dc6/src/index.ts
( (
@ -86,8 +87,6 @@ function babelTransformOptions(isTypeScript: boolean, isModule: boolean, plugins
} }
}) })
]); ]);
} else {
plugins.push([require('@babel/plugin-syntax-import-attributes'), { deprecatedAssertSyntax: true }]);
} }
return { return {

View file

@ -143,7 +143,7 @@ class HtmlReporter implements ReporterV2 {
const shouldOpen = !this._options._isTestServer && (this._open === 'always' || (!ok && this._open === 'on-failure')); const shouldOpen = !this._options._isTestServer && (this._open === 'always' || (!ok && this._open === 'on-failure'));
if (shouldOpen) { if (shouldOpen) {
await showHTMLReport(this._outputFolder, this._host, this._port, singleTestId); await showHTMLReport(this._outputFolder, this._host, this._port, singleTestId);
} else if (this._options._mode === 'test') { } else if (this._options._mode === 'test' && !this._options._isTestServer) {
const packageManagerCommand = getPackageManagerExecCommand(); const packageManagerCommand = getPackageManagerExecCommand();
const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? '' : ' ' + path.relative(process.cwd(), this._outputFolder); const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? '' : ' ' + path.relative(process.cwd(), this._outputFolder);
const hostArg = this._host ? ` --host ${this._host}` : ''; const hostArg = this._host ? ` --host ${this._host}` : '';

View file

@ -84,6 +84,7 @@ export class TestServerDispatcher implements TestServerInterface {
constructor(configLocation: ConfigLocation) { constructor(configLocation: ConfigLocation) {
this._configLocation = configLocation; this._configLocation = configLocation;
this.transport = { this.transport = {
onconnect: () => {},
dispatch: (method, params) => (this as any)[method](params), dispatch: (method, params) => (this as any)[method](params),
onclose: () => { onclose: () => {
if (this._closeOnDisconnect) if (this._closeOnDisconnect)

View file

@ -60,6 +60,8 @@ export class TimeoutManager {
setIgnoreTimeouts() { setIgnoreTimeouts() {
this._ignoreTimeouts = true; this._ignoreTimeouts = true;
if (this._running)
this._updateTimeout(this._running);
} }
interrupt() { interrupt() {

View file

@ -7788,6 +7788,26 @@ interface SnapshotAssertions {
}): void; }): void;
} }
/**
* Represents a location in the source code where [TestCase] or [Suite] is defined.
*/
export interface Location {
/**
* Column number in the source file.
*/
column: number;
/**
* Path to the source file.
*/
file: string;
/**
* Line number in the source file.
*/
line: number;
}
/** /**
* `TestInfo` contains information about currently running test. It is available to test functions, * `TestInfo` contains information about currently running test. It is available to test functions,
* [test.beforeEach([title, hookFunction])](https://playwright.dev/docs/api/class-test#test-before-each), * [test.beforeEach([title, hookFunction])](https://playwright.dev/docs/api/class-test#test-before-each),

View file

@ -15,8 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
import type { TestStatus, Metadata, PlaywrightTestOptions, PlaywrightWorkerOptions, ReporterDescription, FullConfig, FullProject } from './test'; import type { TestStatus, Metadata, PlaywrightTestOptions, PlaywrightWorkerOptions, ReporterDescription, FullConfig, FullProject, Location } from './test';
export type { FullConfig, FullProject, TestStatus } from './test'; export type { FullConfig, FullProject, TestStatus, Location } from './test';
/** /**
* Result of the full test run. * Result of the full test run.
@ -319,26 +319,6 @@ export type JSONReportSTDIOEntry = { text: string } | { buffer: string };
export {}; export {};
/**
* Represents a location in the source code where {@link TestCase} or {@link Suite} is defined.
*/
export interface Location {
/**
* Column number in the source file.
*/
column: number;
/**
* Path to the source file.
*/
file: string;
/**
* Line number in the source file.
*/
line: number;
}
/** /**
* `Suite` is a group of tests. All tests in Playwright Test form the following hierarchy: * `Suite` is a group of tests. All tests in Playwright Test form the following hierarchy:
* - Root suite has a child suite for each {@link FullProject}. * - Root suite has a child suite for each {@link FullProject}.

View file

@ -40,6 +40,7 @@ export type InitializerTraits<T> =
T extends BindingCallChannel ? BindingCallInitializer : T extends BindingCallChannel ? BindingCallInitializer :
T extends WebSocketChannel ? WebSocketInitializer : T extends WebSocketChannel ? WebSocketInitializer :
T extends ResponseChannel ? ResponseInitializer : T extends ResponseChannel ? ResponseInitializer :
T extends WebSocketRouteChannel ? WebSocketRouteInitializer :
T extends RouteChannel ? RouteInitializer : T extends RouteChannel ? RouteInitializer :
T extends RequestChannel ? RequestInitializer : T extends RequestChannel ? RequestInitializer :
T extends ElementHandleChannel ? ElementHandleInitializer : T extends ElementHandleChannel ? ElementHandleInitializer :
@ -77,6 +78,7 @@ export type EventsTraits<T> =
T extends BindingCallChannel ? BindingCallEvents : T extends BindingCallChannel ? BindingCallEvents :
T extends WebSocketChannel ? WebSocketEvents : T extends WebSocketChannel ? WebSocketEvents :
T extends ResponseChannel ? ResponseEvents : T extends ResponseChannel ? ResponseEvents :
T extends WebSocketRouteChannel ? WebSocketRouteEvents :
T extends RouteChannel ? RouteEvents : T extends RouteChannel ? RouteEvents :
T extends RequestChannel ? RequestEvents : T extends RequestChannel ? RequestEvents :
T extends ElementHandleChannel ? ElementHandleEvents : T extends ElementHandleChannel ? ElementHandleEvents :
@ -114,6 +116,7 @@ export type EventTargetTraits<T> =
T extends BindingCallChannel ? BindingCallEventTarget : T extends BindingCallChannel ? BindingCallEventTarget :
T extends WebSocketChannel ? WebSocketEventTarget : T extends WebSocketChannel ? WebSocketEventTarget :
T extends ResponseChannel ? ResponseEventTarget : T extends ResponseChannel ? ResponseEventTarget :
T extends WebSocketRouteChannel ? WebSocketRouteEventTarget :
T extends RouteChannel ? RouteEventTarget : T extends RouteChannel ? RouteEventTarget :
T extends RequestChannel ? RequestEventTarget : T extends RequestChannel ? RequestEventTarget :
T extends ElementHandleChannel ? ElementHandleEventTarget : T extends ElementHandleChannel ? ElementHandleEventTarget :
@ -1493,6 +1496,7 @@ export interface BrowserContextEventTarget {
on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this; on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this;
on(event: 'pageError', callback: (params: BrowserContextPageErrorEvent) => void): this; on(event: 'pageError', callback: (params: BrowserContextPageErrorEvent) => void): this;
on(event: 'route', callback: (params: BrowserContextRouteEvent) => void): this; on(event: 'route', callback: (params: BrowserContextRouteEvent) => void): this;
on(event: 'webSocketRoute', callback: (params: BrowserContextWebSocketRouteEvent) => void): this;
on(event: 'video', callback: (params: BrowserContextVideoEvent) => void): this; on(event: 'video', callback: (params: BrowserContextVideoEvent) => void): this;
on(event: 'backgroundPage', callback: (params: BrowserContextBackgroundPageEvent) => void): this; on(event: 'backgroundPage', callback: (params: BrowserContextBackgroundPageEvent) => void): this;
on(event: 'serviceWorker', callback: (params: BrowserContextServiceWorkerEvent) => void): this; on(event: 'serviceWorker', callback: (params: BrowserContextServiceWorkerEvent) => void): this;
@ -1518,10 +1522,11 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
setGeolocation(params: BrowserContextSetGeolocationParams, metadata?: CallMetadata): Promise<BrowserContextSetGeolocationResult>; setGeolocation(params: BrowserContextSetGeolocationParams, metadata?: CallMetadata): Promise<BrowserContextSetGeolocationResult>;
setHTTPCredentials(params: BrowserContextSetHTTPCredentialsParams, metadata?: CallMetadata): Promise<BrowserContextSetHTTPCredentialsResult>; setHTTPCredentials(params: BrowserContextSetHTTPCredentialsParams, metadata?: CallMetadata): Promise<BrowserContextSetHTTPCredentialsResult>;
setNetworkInterceptionPatterns(params: BrowserContextSetNetworkInterceptionPatternsParams, metadata?: CallMetadata): Promise<BrowserContextSetNetworkInterceptionPatternsResult>; setNetworkInterceptionPatterns(params: BrowserContextSetNetworkInterceptionPatternsParams, metadata?: CallMetadata): Promise<BrowserContextSetNetworkInterceptionPatternsResult>;
setWebSocketInterceptionPatterns(params: BrowserContextSetWebSocketInterceptionPatternsParams, metadata?: CallMetadata): Promise<BrowserContextSetWebSocketInterceptionPatternsResult>;
setOffline(params: BrowserContextSetOfflineParams, metadata?: CallMetadata): Promise<BrowserContextSetOfflineResult>; setOffline(params: BrowserContextSetOfflineParams, metadata?: CallMetadata): Promise<BrowserContextSetOfflineResult>;
storageState(params?: BrowserContextStorageStateParams, metadata?: CallMetadata): Promise<BrowserContextStorageStateResult>; storageState(params?: BrowserContextStorageStateParams, metadata?: CallMetadata): Promise<BrowserContextStorageStateResult>;
pause(params?: BrowserContextPauseParams, metadata?: CallMetadata): Promise<BrowserContextPauseResult>; pause(params?: BrowserContextPauseParams, metadata?: CallMetadata): Promise<BrowserContextPauseResult>;
recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: CallMetadata): Promise<BrowserContextRecorderSupplementEnableResult>; enableRecorder(params: BrowserContextEnableRecorderParams, metadata?: CallMetadata): Promise<BrowserContextEnableRecorderResult>;
newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: CallMetadata): Promise<BrowserContextNewCDPSessionResult>; newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: CallMetadata): Promise<BrowserContextNewCDPSessionResult>;
harStart(params: BrowserContextHarStartParams, metadata?: CallMetadata): Promise<BrowserContextHarStartResult>; harStart(params: BrowserContextHarStartParams, metadata?: CallMetadata): Promise<BrowserContextHarStartResult>;
harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>; harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>;
@ -1563,6 +1568,9 @@ export type BrowserContextPageErrorEvent = {
export type BrowserContextRouteEvent = { export type BrowserContextRouteEvent = {
route: RouteChannel, route: RouteChannel,
}; };
export type BrowserContextWebSocketRouteEvent = {
webSocketRoute: WebSocketRouteChannel,
};
export type BrowserContextVideoEvent = { export type BrowserContextVideoEvent = {
artifact: ArtifactChannel, artifact: ArtifactChannel,
}; };
@ -1731,6 +1739,17 @@ export type BrowserContextSetNetworkInterceptionPatternsOptions = {
}; };
export type BrowserContextSetNetworkInterceptionPatternsResult = void; export type BrowserContextSetNetworkInterceptionPatternsResult = void;
export type BrowserContextSetWebSocketInterceptionPatternsParams = {
patterns: {
glob?: string,
regexSource?: string,
regexFlags?: string,
}[],
};
export type BrowserContextSetWebSocketInterceptionPatternsOptions = {
};
export type BrowserContextSetWebSocketInterceptionPatternsResult = void;
export type BrowserContextSetOfflineParams = { export type BrowserContextSetOfflineParams = {
offline: boolean, offline: boolean,
}; };
@ -1747,9 +1766,10 @@ export type BrowserContextStorageStateResult = {
export type BrowserContextPauseParams = {}; export type BrowserContextPauseParams = {};
export type BrowserContextPauseOptions = {}; export type BrowserContextPauseOptions = {};
export type BrowserContextPauseResult = void; export type BrowserContextPauseResult = void;
export type BrowserContextRecorderSupplementEnableParams = { export type BrowserContextEnableRecorderParams = {
language?: string, language?: string,
mode?: 'inspecting' | 'recording', mode?: 'inspecting' | 'recording',
codegenMode?: 'actions' | 'trace-events',
pauseOnNextStatement?: boolean, pauseOnNextStatement?: boolean,
testIdAttributeName?: string, testIdAttributeName?: string,
launchOptions?: any, launchOptions?: any,
@ -1759,9 +1779,10 @@ export type BrowserContextRecorderSupplementEnableParams = {
outputFile?: string, outputFile?: string,
omitCallTracking?: boolean, omitCallTracking?: boolean,
}; };
export type BrowserContextRecorderSupplementEnableOptions = { export type BrowserContextEnableRecorderOptions = {
language?: string, language?: string,
mode?: 'inspecting' | 'recording', mode?: 'inspecting' | 'recording',
codegenMode?: 'actions' | 'trace-events',
pauseOnNextStatement?: boolean, pauseOnNextStatement?: boolean,
testIdAttributeName?: string, testIdAttributeName?: string,
launchOptions?: any, launchOptions?: any,
@ -1771,7 +1792,7 @@ export type BrowserContextRecorderSupplementEnableOptions = {
outputFile?: string, outputFile?: string,
omitCallTracking?: boolean, omitCallTracking?: boolean,
}; };
export type BrowserContextRecorderSupplementEnableResult = void; export type BrowserContextEnableRecorderResult = void;
export type BrowserContextNewCDPSessionParams = { export type BrowserContextNewCDPSessionParams = {
page?: PageChannel, page?: PageChannel,
frame?: FrameChannel, frame?: FrameChannel,
@ -1890,6 +1911,7 @@ export interface BrowserContextEvents {
'page': BrowserContextPageEvent; 'page': BrowserContextPageEvent;
'pageError': BrowserContextPageErrorEvent; 'pageError': BrowserContextPageErrorEvent;
'route': BrowserContextRouteEvent; 'route': BrowserContextRouteEvent;
'webSocketRoute': BrowserContextWebSocketRouteEvent;
'video': BrowserContextVideoEvent; 'video': BrowserContextVideoEvent;
'backgroundPage': BrowserContextBackgroundPageEvent; 'backgroundPage': BrowserContextBackgroundPageEvent;
'serviceWorker': BrowserContextServiceWorkerEvent; 'serviceWorker': BrowserContextServiceWorkerEvent;
@ -1919,6 +1941,7 @@ export interface PageEventTarget {
on(event: 'frameDetached', callback: (params: PageFrameDetachedEvent) => void): this; on(event: 'frameDetached', callback: (params: PageFrameDetachedEvent) => void): this;
on(event: 'locatorHandlerTriggered', callback: (params: PageLocatorHandlerTriggeredEvent) => void): this; on(event: 'locatorHandlerTriggered', callback: (params: PageLocatorHandlerTriggeredEvent) => void): this;
on(event: 'route', callback: (params: PageRouteEvent) => void): this; on(event: 'route', callback: (params: PageRouteEvent) => void): this;
on(event: 'webSocketRoute', callback: (params: PageWebSocketRouteEvent) => void): this;
on(event: 'video', callback: (params: PageVideoEvent) => void): this; on(event: 'video', callback: (params: PageVideoEvent) => void): this;
on(event: 'webSocket', callback: (params: PageWebSocketEvent) => void): this; on(event: 'webSocket', callback: (params: PageWebSocketEvent) => void): this;
on(event: 'worker', callback: (params: PageWorkerEvent) => void): this; on(event: 'worker', callback: (params: PageWorkerEvent) => void): this;
@ -1942,6 +1965,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
screenshot(params: PageScreenshotParams, metadata?: CallMetadata): Promise<PageScreenshotResult>; screenshot(params: PageScreenshotParams, metadata?: CallMetadata): Promise<PageScreenshotResult>;
setExtraHTTPHeaders(params: PageSetExtraHTTPHeadersParams, metadata?: CallMetadata): Promise<PageSetExtraHTTPHeadersResult>; setExtraHTTPHeaders(params: PageSetExtraHTTPHeadersParams, metadata?: CallMetadata): Promise<PageSetExtraHTTPHeadersResult>;
setNetworkInterceptionPatterns(params: PageSetNetworkInterceptionPatternsParams, metadata?: CallMetadata): Promise<PageSetNetworkInterceptionPatternsResult>; setNetworkInterceptionPatterns(params: PageSetNetworkInterceptionPatternsParams, metadata?: CallMetadata): Promise<PageSetNetworkInterceptionPatternsResult>;
setWebSocketInterceptionPatterns(params: PageSetWebSocketInterceptionPatternsParams, metadata?: CallMetadata): Promise<PageSetWebSocketInterceptionPatternsResult>;
setViewportSize(params: PageSetViewportSizeParams, metadata?: CallMetadata): Promise<PageSetViewportSizeResult>; setViewportSize(params: PageSetViewportSizeParams, metadata?: CallMetadata): Promise<PageSetViewportSizeResult>;
keyboardDown(params: PageKeyboardDownParams, metadata?: CallMetadata): Promise<PageKeyboardDownResult>; keyboardDown(params: PageKeyboardDownParams, metadata?: CallMetadata): Promise<PageKeyboardDownResult>;
keyboardUp(params: PageKeyboardUpParams, metadata?: CallMetadata): Promise<PageKeyboardUpResult>; keyboardUp(params: PageKeyboardUpParams, metadata?: CallMetadata): Promise<PageKeyboardUpResult>;
@ -1989,6 +2013,9 @@ export type PageLocatorHandlerTriggeredEvent = {
export type PageRouteEvent = { export type PageRouteEvent = {
route: RouteChannel, route: RouteChannel,
}; };
export type PageWebSocketRouteEvent = {
webSocketRoute: WebSocketRouteChannel,
};
export type PageVideoEvent = { export type PageVideoEvent = {
artifact: ArtifactChannel, artifact: ArtifactChannel,
}; };
@ -2221,6 +2248,17 @@ export type PageSetNetworkInterceptionPatternsOptions = {
}; };
export type PageSetNetworkInterceptionPatternsResult = void; export type PageSetNetworkInterceptionPatternsResult = void;
export type PageSetWebSocketInterceptionPatternsParams = {
patterns: {
glob?: string,
regexSource?: string,
regexFlags?: string,
}[],
};
export type PageSetWebSocketInterceptionPatternsOptions = {
};
export type PageSetWebSocketInterceptionPatternsResult = void;
export type PageSetViewportSizeParams = { export type PageSetViewportSizeParams = {
viewportSize: { viewportSize: {
width: number, width: number,
@ -2448,6 +2486,7 @@ export interface PageEvents {
'frameDetached': PageFrameDetachedEvent; 'frameDetached': PageFrameDetachedEvent;
'locatorHandlerTriggered': PageLocatorHandlerTriggeredEvent; 'locatorHandlerTriggered': PageLocatorHandlerTriggeredEvent;
'route': PageRouteEvent; 'route': PageRouteEvent;
'webSocketRoute': PageWebSocketRouteEvent;
'video': PageVideoEvent; 'video': PageVideoEvent;
'webSocket': PageWebSocketEvent; 'webSocket': PageWebSocketEvent;
'worker': PageWorkerEvent; 'worker': PageWorkerEvent;
@ -3732,7 +3771,6 @@ export type RouteRedirectNavigationRequestOptions = {
export type RouteRedirectNavigationRequestResult = void; export type RouteRedirectNavigationRequestResult = void;
export type RouteAbortParams = { export type RouteAbortParams = {
errorCode?: string, errorCode?: string,
requestUrl: string,
}; };
export type RouteAbortOptions = { export type RouteAbortOptions = {
errorCode?: string, errorCode?: string,
@ -3743,7 +3781,6 @@ export type RouteContinueParams = {
method?: string, method?: string,
headers?: NameValue[], headers?: NameValue[],
postData?: Binary, postData?: Binary,
requestUrl: string,
isFallback: boolean, isFallback: boolean,
}; };
export type RouteContinueOptions = { export type RouteContinueOptions = {
@ -3759,7 +3796,6 @@ export type RouteFulfillParams = {
body?: string, body?: string,
isBase64?: boolean, isBase64?: boolean,
fetchResponseUid?: string, fetchResponseUid?: string,
requestUrl: string,
}; };
export type RouteFulfillOptions = { export type RouteFulfillOptions = {
status?: number, status?: number,
@ -3773,6 +3809,70 @@ export type RouteFulfillResult = void;
export interface RouteEvents { export interface RouteEvents {
} }
// ----------- WebSocketRoute -----------
export type WebSocketRouteInitializer = {
url: string,
};
export interface WebSocketRouteEventTarget {
on(event: 'messageFromPage', callback: (params: WebSocketRouteMessageFromPageEvent) => void): this;
on(event: 'messageFromServer', callback: (params: WebSocketRouteMessageFromServerEvent) => void): this;
on(event: 'close', callback: (params: WebSocketRouteCloseEvent) => void): this;
}
export interface WebSocketRouteChannel extends WebSocketRouteEventTarget, Channel {
_type_WebSocketRoute: boolean;
connect(params?: WebSocketRouteConnectParams, metadata?: CallMetadata): Promise<WebSocketRouteConnectResult>;
ensureOpened(params?: WebSocketRouteEnsureOpenedParams, metadata?: CallMetadata): Promise<WebSocketRouteEnsureOpenedResult>;
sendToPage(params: WebSocketRouteSendToPageParams, metadata?: CallMetadata): Promise<WebSocketRouteSendToPageResult>;
sendToServer(params: WebSocketRouteSendToServerParams, metadata?: CallMetadata): Promise<WebSocketRouteSendToServerResult>;
close(params: WebSocketRouteCloseParams, metadata?: CallMetadata): Promise<WebSocketRouteCloseResult>;
}
export type WebSocketRouteMessageFromPageEvent = {
message: string,
isBase64: boolean,
};
export type WebSocketRouteMessageFromServerEvent = {
message: string,
isBase64: boolean,
};
export type WebSocketRouteCloseEvent = {};
export type WebSocketRouteConnectParams = {};
export type WebSocketRouteConnectOptions = {};
export type WebSocketRouteConnectResult = void;
export type WebSocketRouteEnsureOpenedParams = {};
export type WebSocketRouteEnsureOpenedOptions = {};
export type WebSocketRouteEnsureOpenedResult = void;
export type WebSocketRouteSendToPageParams = {
message: string,
isBase64: boolean,
};
export type WebSocketRouteSendToPageOptions = {
};
export type WebSocketRouteSendToPageResult = void;
export type WebSocketRouteSendToServerParams = {
message: string,
isBase64: boolean,
};
export type WebSocketRouteSendToServerOptions = {
};
export type WebSocketRouteSendToServerResult = void;
export type WebSocketRouteCloseParams = {
code?: number,
reason?: string,
};
export type WebSocketRouteCloseOptions = {
code?: number,
reason?: string,
};
export type WebSocketRouteCloseResult = void;
export interface WebSocketRouteEvents {
'messageFromPage': WebSocketRouteMessageFromPageEvent;
'messageFromServer': WebSocketRouteMessageFromServerEvent;
'close': WebSocketRouteCloseEvent;
}
export type ResourceTiming = { export type ResourceTiming = {
startTime: number, startTime: number,
domainLookupStart: number, domainLookupStart: number,

View file

@ -1160,6 +1160,17 @@ BrowserContext:
regexSource: string? regexSource: string?
regexFlags: string? regexFlags: string?
setWebSocketInterceptionPatterns:
parameters:
patterns:
type: array
items:
type: object
properties:
glob: string?
regexSource: string?
regexFlags: string?
setOffline: setOffline:
parameters: parameters:
offline: boolean offline: boolean
@ -1176,7 +1187,7 @@ BrowserContext:
pause: pause:
experimental: True experimental: True
recorderSupplementEnable: enableRecorder:
experimental: True experimental: True
parameters: parameters:
language: string? language: string?
@ -1185,6 +1196,11 @@ BrowserContext:
literals: literals:
- inspecting - inspecting
- recording - recording
codegenMode:
type: enum?
literals:
- actions
- trace-events
pauseOnNextStatement: boolean? pauseOnNextStatement: boolean?
testIdAttributeName: string? testIdAttributeName: string?
launchOptions: json? launchOptions: json?
@ -1305,6 +1321,10 @@ BrowserContext:
parameters: parameters:
route: Route route: Route
webSocketRoute:
parameters:
webSocketRoute: WebSocketRoute
video: video:
parameters: parameters:
artifact: Artifact artifact: Artifact
@ -1520,6 +1540,17 @@ Page:
regexSource: string? regexSource: string?
regexFlags: string? regexFlags: string?
setWebSocketInterceptionPatterns:
parameters:
patterns:
type: array
items:
type: object
properties:
glob: string?
regexSource: string?
regexFlags: string?
setViewportSize: setViewportSize:
parameters: parameters:
viewportSize: viewportSize:
@ -1771,6 +1802,10 @@ Page:
parameters: parameters:
route: Route route: Route
webSocketRoute:
parameters:
webSocketRoute: WebSocketRoute
video: video:
parameters: parameters:
artifact: Artifact artifact: Artifact
@ -2911,7 +2946,6 @@ Route:
abort: abort:
parameters: parameters:
errorCode: string? errorCode: string?
requestUrl: string
continue: continue:
parameters: parameters:
@ -2921,7 +2955,6 @@ Route:
type: array? type: array?
items: NameValue items: NameValue
postData: binary? postData: binary?
requestUrl: string
isFallback: boolean isFallback: boolean
fulfill: fulfill:
@ -2934,7 +2967,49 @@ Route:
body: string? body: string?
isBase64: boolean? isBase64: boolean?
fetchResponseUid: string? fetchResponseUid: string?
requestUrl: string
WebSocketRoute:
type: interface
initializer:
url: string
commands:
connect:
ensureOpened:
sendToPage:
parameters:
message: string
isBase64: boolean
sendToServer:
parameters:
message: string
isBase64: boolean
close:
parameters:
code: number?
reason: string?
events:
messageFromPage:
parameters:
message: string
isBase64: boolean
messageFromServer:
parameters:
message: string
isBase64: boolean
close:
ResourceTiming: ResourceTiming:
type: object type: object

View file

@ -27,7 +27,10 @@ export const Main: React.FC = ({
const [mode, setMode] = React.useState<Mode>('none'); const [mode, setMode] = React.useState<Mode>('none');
window.playwrightSetMode = setMode; window.playwrightSetMode = setMode;
window.playwrightSetSources = setSources; window.playwrightSetSources = React.useCallback((sources: Source[]) => {
setSources(sources);
window.playwrightSourcesEchoForTest = sources;
}, []);
window.playwrightSetPaused = setPaused; window.playwrightSetPaused = setPaused;
window.playwrightUpdateLogs = callLogs => { window.playwrightUpdateLogs = callLogs => {
setLog(log => { setLog(log => {
@ -40,6 +43,5 @@ export const Main: React.FC = ({
}); });
}; };
window.playwrightSourcesEchoForTest = sources;
return <Recorder sources={sources} paused={paused} log={log} mode={mode} />; return <Recorder sources={sources} paused={paused} log={log} mode={mode} />;
}; };

View file

@ -47,12 +47,14 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, clientI
} }
set.add(traceUrl); set.add(traceUrl);
const isRecorderMode = traceUrl.includes('/playwright-recorder-trace-');
const traceModel = new TraceModel(); const traceModel = new TraceModel();
try { try {
// Allow 10% to hop from sw to page. // Allow 10% to hop from sw to page.
const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]); const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]);
const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl) : new ZipTraceModelBackend(traceUrl, fetchProgress); const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl) : new ZipTraceModelBackend(traceUrl, fetchProgress);
await traceModel.load(backend, unzipProgress); await traceModel.load(backend, isRecorderMode, unzipProgress);
} catch (error: any) { } catch (error: any) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(error); console.error(error);

View file

@ -0,0 +1,285 @@
// Copyright 2014 The Chromium Authors. All rights reserved.
// Modifications copyright (c) Microsoft Corporation.
//
// 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.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// 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
// OWNER 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.
/*
* Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org>
* Copyright (C) 2011 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. 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.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS 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 APPLE OR ITS 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.
*/
import type { Entry } from '@trace/har';
// The following function is derived from Chromium's source code
// https://github.com/ChromeDevTools/devtools-frontend/blob/83cbe41b4107e188a1f66fdf6ea3a9cca42587c6/front_end/panels/network/NetworkLogView.ts#L2363
export async function generateCurlCommand(resource: Entry): Promise<string> {
const platform = navigator.platform.includes('Win') ? 'win' : 'unix';
let command: string[] = [];
// Most of these headers are derived from the URL and are automatically added by cURL.
// The |Accept-Encoding| header is ignored to prevent decompression errors. crbug.com/1015321
const ignoredHeaders =
new Set<string>(['accept-encoding', 'host', 'method', 'path', 'scheme', 'version', 'authority', 'protocol']);
function escapeStringWin(str: string): string {
/* Always escape the " characters so that we can use caret escaping.
Because cmd.exe parser and MS Crt arguments parsers use some of the
same escape characters, they can interact with each other in
horrible ways, the order of operations is critical.
Replace \ with \\ first because it is an escape character for certain
conditions in both parsers.
Replace all " with \" to ensure the first parser does not remove it.
Then escape all characters we are not sure about with ^ to ensure it
gets to MS Crt parser safely.
The % character is special because MS Crt parser will try and look for
ENV variables and fill them in its place. We cannot escape them with %
and cannot escape them with ^ (because it's cmd.exe's escape not MS Crt
parser); So we can get cmd.exe parser to escape the character after it,
if it is followed by a valid beginning character of an ENV variable.
This ensures we do not try and double escape another ^ if it was placed
by the previous replace.
Lastly we replace new lines with ^ and TWO new lines because the first
new line is there to enact the escape command the second is the character
to escape (in this case new line).
*/
const encapsChars = '^"';
return encapsChars +
str.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/[^a-zA-Z0-9\s_\-:=+~'\/.',?;()*`]/g, '^$&')
.replace(/%(?=[a-zA-Z0-9_])/g, '%^')
.replace(/\r?\n/g, '^\n\n') +
encapsChars;
}
function escapeStringPosix(str: string): string {
function escapeCharacter(x: string): string {
const code = x.charCodeAt(0);
let hexString = code.toString(16);
// Zero pad to four digits to comply with ANSI-C Quoting:
// http://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
while (hexString.length < 4)
hexString = '0' + hexString;
return '\\u' + hexString;
}
if (/[\0-\x1F\x7F-\x9F!]|\'/.test(str)) {
// Use ANSI-C quoting syntax.
return '$\'' +
str.replace(/\\/g, '\\\\')
.replace(/\'/g, '\\\'')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/[\0-\x1F\x7F-\x9F!]/g, escapeCharacter) +
'\'';
}
// Use single quote syntax.
return '\'' + str + '\'';
}
// cURL command expected to run on the same platform that DevTools run
// (it may be different from the inspected page platform).
const escapeString = platform === 'win' ? escapeStringWin : escapeStringPosix;
command.push(escapeString(resource.request.url).replace(/[[{}\]]/g, '\\$&'));
let inferredMethod = 'GET';
const data = [];
const formData = await fetchRequestPostData(resource);
if (formData) {
// Note that formData is not necessarily urlencoded because it might for example
// come from a fetch request made with an explicitly unencoded body.
data.push('--data-raw ' + escapeString(formData));
ignoredHeaders.add('content-length');
inferredMethod = 'POST';
}
if (resource.request.method !== inferredMethod)
command.push('-X ' + escapeString(resource.request.method));
const requestHeaders = resource.request.headers;
for (let i = 0; i < requestHeaders.length; i++) {
const header = requestHeaders[i];
const name = header.name.replace(/^:/, ''); // Translate SPDY v3 headers to HTTP headers.
if (ignoredHeaders.has(name.toLowerCase()))
continue;
if (header.value.trim()) {
command.push('-H ' + escapeString(name + ': ' + header.value));
} else {
// A header passed with -H with no value or only whitespace as its
// value tells curl to not set the header at all. To post an empty
// header, you have to terminate it with a semicolon.
command.push('-H ' + escapeString(name + ';'));
}
}
command = command.concat(data);
return 'curl ' + command.join(command.length >= 3 ? (platform === 'win' ? ' ^\n ' : ' \\\n ') : ' ');
}
const enum FetchStyle {
BROWSER = 0,
NODE_JS = 1,
}
export async function generateFetchCall(resource: Entry, style: FetchStyle = FetchStyle.BROWSER): Promise<string> {
const ignoredHeaders = new Set<string>([
// Internal headers
'method',
'path',
'scheme',
'version',
// Unsafe headers
// Keep this list synchronized with src/net/http/http_util.cc
'accept-charset',
'accept-encoding',
'access-control-request-headers',
'access-control-request-method',
'connection',
'content-length',
'cookie',
'cookie2',
'date',
'dnt',
'expect',
'host',
'keep-alive',
'origin',
'referer',
'te',
'trailer',
'transfer-encoding',
'upgrade',
'via',
// TODO(phistuck) - remove this once crbug.com/571722 is fixed.
'user-agent',
]);
const credentialHeaders = new Set<string>(['cookie', 'authorization']);
const url = JSON.stringify(resource.request.url);
const requestHeaders = resource.request.headers;
const headerData: Headers = requestHeaders.reduce((result, header) => {
const name = header.name;
if (!ignoredHeaders.has(name.toLowerCase()) && !name.includes(':'))
result.append(name, header.value);
return result;
}, new Headers());
const headers: HeadersInit = {};
for (const headerArray of headerData)
headers[headerArray[0]] = headerArray[1];
const credentials = resource.request.cookies.length ||
requestHeaders.some(({ name }) => credentialHeaders.has(name.toLowerCase())) ?
'include' :
'omit';
const referrerHeader = requestHeaders.find(({ name }) => name.toLowerCase() === 'referer');
const referrer = referrerHeader ? referrerHeader.value : void 0;
const requestBody = await fetchRequestPostData(resource);
const fetchOptions: RequestInit = {
headers: Object.keys(headers).length ? headers : void 0,
referrer,
body: requestBody,
method: resource.request.method,
mode: 'cors',
};
if (style === FetchStyle.NODE_JS) {
const cookieHeader = requestHeaders.find(header => header.name.toLowerCase() === 'cookie');
const extraHeaders: HeadersInit = {};
// According to https://www.npmjs.com/package/node-fetch#class-request the
// following properties are not implemented in Node.js.
delete fetchOptions.mode;
if (cookieHeader)
extraHeaders['cookie'] = cookieHeader.value;
if (referrer) {
delete fetchOptions.referrer;
extraHeaders['Referer'] = referrer;
}
if (Object.keys(extraHeaders).length) {
fetchOptions.headers = {
...headers,
...extraHeaders,
};
}
} else {
fetchOptions.credentials = credentials;
}
const options = JSON.stringify(fetchOptions, null, 2);
return `fetch(${url}, ${options});`;
}
async function fetchRequestPostData(resource: Entry) {
return resource.request.postData?._sha1 ? await fetch(`sha1/${resource.request.postData._sha1}`).then(r => r.text()) : resource.request.postData?.text;
}

View file

@ -15,7 +15,7 @@
*/ */
import { parseClientSideCallMetadata } from '../../../packages/playwright-core/src/utils/isomorphic/traceUtils'; import { parseClientSideCallMetadata } from '../../../packages/playwright-core/src/utils/isomorphic/traceUtils';
import type { ContextEntry } from './entries'; import type { ActionEntry, ContextEntry } from './entries';
import { createEmptyContext } from './entries'; import { createEmptyContext } from './entries';
import { SnapshotStorage } from './snapshotStorage'; import { SnapshotStorage } from './snapshotStorage';
import { TraceModernizer } from './traceModernizer'; import { TraceModernizer } from './traceModernizer';
@ -38,7 +38,7 @@ export class TraceModel {
constructor() { constructor() {
} }
async load(backend: TraceModelBackend, unzipProgress: (done: number, total: number) => void) { async load(backend: TraceModelBackend, isRecorderMode: boolean, unzipProgress: (done: number, total: number) => void) {
this._backend = backend; this._backend = backend;
const ordinals: string[] = []; const ordinals: string[] = [];
@ -72,7 +72,9 @@ export class TraceModel {
modernizer.appendTrace(network); modernizer.appendTrace(network);
unzipProgress(++done, total); unzipProgress(++done, total);
contextEntry.actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime); const actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime);
contextEntry.actions = isRecorderMode ? collapseActionsForRecorder(actions) : actions;
if (!backend.isLive()) { if (!backend.isLive()) {
// Terminate actions w/o after event gracefully. // Terminate actions w/o after event gracefully.
// This would close after hooks event that has not been closed because // This would close after hooks event that has not been closed because
@ -132,3 +134,19 @@ function stripEncodingFromContentType(contentType: string) {
return charset[1]; return charset[1];
return contentType; return contentType;
} }
function collapseActionsForRecorder(actions: ActionEntry[]): ActionEntry[] {
const result: ActionEntry[] = [];
for (const action of actions) {
const lastAction = result[result.length - 1];
const isSameAction = lastAction && lastAction.method === action.method && lastAction.pageId === action.pageId;
const isSameSelector = lastAction && 'selector' in lastAction.params && 'selector' in action.params && action.params.selector === lastAction.params.selector;
const shouldMerge = isSameAction && (action.method === 'goto' || (action.method === 'fill' && isSameSelector));
if (!shouldMerge) {
result.push(action);
continue;
}
result[result.length - 1] = action;
}
return result;
}

View file

@ -6,3 +6,4 @@
../entries.ts ../entries.ts
../geometry.ts ../geometry.ts
../../../playwright/src/isomorphic/** ../../../playwright/src/isomorphic/**
../third_party/devtools.ts

View file

@ -36,6 +36,8 @@
.call-section { .call-section {
padding-left: 6px; padding-left: 6px;
padding-top: 2px;
margin-top: 2px;
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
font-size: 10px; font-size: 10px;
@ -53,9 +55,8 @@
align-items: center; align-items: center;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
line-height: 18px; line-height: 20px;
white-space: nowrap; white-space: nowrap;
max-height: 18px;
} }
.call-line:not(:hover) .toolbar-button.copy { .call-line:not(:hover) .toolbar-button.copy {
@ -64,7 +65,8 @@
.call-line .toolbar-button.copy { .call-line .toolbar-button.copy {
margin-left: 5px; margin-left: 5px;
transform: scale(0.8); margin-top: -2px;
margin-bottom: -2px;
} }
.call-value { .call-value {

View file

@ -18,19 +18,22 @@ import * as React from 'react';
import { ToolbarButton } from '@web/components/toolbarButton'; import { ToolbarButton } from '@web/components/toolbarButton';
export const CopyToClipboard: React.FunctionComponent<{ export const CopyToClipboard: React.FunctionComponent<{
value: string, value: string | (() => Promise<string>),
description?: string, description?: string,
}> = ({ value, description }) => { }> = ({ value, description }) => {
const [icon, setIcon] = React.useState('copy'); const [icon, setIcon] = React.useState('copy');
const handleCopy = React.useCallback(() => { const handleCopy = React.useCallback(() => {
navigator.clipboard.writeText(value).then(() => { const valuePromise = typeof value === 'function' ? value() : Promise.resolve(value);
setIcon('check'); valuePromise.then(value => {
setTimeout(() => { navigator.clipboard.writeText(value).then(() => {
setIcon('copy'); setIcon('check');
}, 3000); setTimeout(() => {
}, () => { setIcon('copy');
setIcon('close'); }, 3000);
}, () => {
setIcon('close');
});
}); });
}, [value]); }, [value]);

View file

@ -312,7 +312,7 @@ function monotonicTimeDeltaBetweenLibraryAndRunner(nonPrimaryContexts: ContextEn
for (const action of context.actions) { for (const action of context.actions) {
if (!action.startTime) if (!action.startTime)
continue; continue;
const key = matchByStepId ? action.stepId! : `${action.apiName}@${(action as any).wallTime}`; const key = matchByStepId ? action.callId! : `${action.apiName}@${(action as any).wallTime}`;
const libraryAction = libraryActions.get(key); const libraryAction = libraryActions.get(key);
if (libraryAction) if (libraryAction)
return action.startTime - libraryAction.startTime; return action.startTime - libraryAction.startTime;

View file

@ -49,6 +49,15 @@
overflow: hidden; overflow: hidden;
} }
.network-request-details-copy {
display: flex;
margin-left: 10px;
}
.network-request-details-copy button {
border-radius: 4px
}
.network-font-preview { .network-font-preview {
font-family: font-preview; font-family: font-preview;
font-size: 30px; font-size: 30px;

View file

@ -20,6 +20,8 @@ import './networkResourceDetails.css';
import { TabbedPane } from '@web/components/tabbedPane'; import { TabbedPane } from '@web/components/tabbedPane';
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper'; import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
import { ToolbarButton } from '@web/components/toolbarButton'; import { ToolbarButton } from '@web/components/toolbarButton';
import { generateCurlCommand, generateFetchCall } from '../third_party/devtools';
import { CopyToClipboard } from './copyToClipboard';
export const NetworkResourceDetails: React.FunctionComponent<{ export const NetworkResourceDetails: React.FunctionComponent<{
resource: ResourceSnapshot; resource: ResourceSnapshot;
@ -90,6 +92,13 @@ const RequestTab: React.FunctionComponent<{
</> : null} </> : null}
<div className='network-request-details-header'>Request Headers</div> <div className='network-request-details-header'>Request Headers</div>
<div className='network-request-details-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div> <div className='network-request-details-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
<div className='network-request-details-header'>Copy Request</div>
<div className='network-request-details-copy'>
As cURL: <CopyToClipboard description='Copy as cURL' value={() => generateCurlCommand(resource)}/>
</div>
<div className='network-request-details-copy'>
As Fetch: <CopyToClipboard description='Copy as Fetch' value={() => generateFetchCall(resource)}/>
</div>
{requestBody && <div className='network-request-details-header'>Request Body</div>} {requestBody && <div className='network-request-details-header'>Request Body</div>}
{requestBody && <CodeMirrorWrapper text={requestBody.text} mimeType={requestBody.mimeType} readOnly lineNumbers={true}/>} {requestBody && <CodeMirrorWrapper text={requestBody.text} mimeType={requestBody.mimeType} readOnly lineNumbers={true}/>}
</div>; </div>;

View file

@ -102,6 +102,7 @@ export const TraceView: React.FC<{
showSourcesFirst={true} showSourcesFirst={true}
fallbackLocation={fallbackLocation} fallbackLocation={fallbackLocation}
isLive={true} isLive={true}
hideTimeline={true}
/>; />;
}; };
@ -163,6 +164,7 @@ class Connection {
if (method === 'setSources') { if (method === 'setSources') {
const { sources } = params as { sources: Source[] }; const { sources } = params as { sources: Source[] };
this._options.setSources(sources); this._options.setSources(sources);
window.playwrightSourcesEchoForTest = sources;
} }
} }
} }

View file

@ -105,7 +105,6 @@ export const UIModeView: React.FC<{}> = ({
const [singleWorker, setSingleWorker] = React.useState(queryParams.workers === '1'); const [singleWorker, setSingleWorker] = React.useState(queryParams.workers === '1');
const [showBrowser, setShowBrowser] = React.useState(queryParams.headed); const [showBrowser, setShowBrowser] = React.useState(queryParams.headed);
const [updateSnapshots, setUpdateSnapshots] = React.useState(queryParams.updateSnapshots === 'all'); const [updateSnapshots, setUpdateSnapshots] = React.useState(queryParams.updateSnapshots === 'all');
const [showRouteActions, setShowRouteActions] = useSetting('show-route-actions', true);
const [darkMode, setDarkMode] = useDarkModeSetting(); const [darkMode, setDarkMode] = useDarkModeSetting();
const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false); const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false);
@ -526,7 +525,6 @@ export const UIModeView: React.FC<{}> = ({
</Toolbar> </Toolbar>
{settingsVisible && <SettingsView settings={[ {settingsVisible && <SettingsView settings={[
{ value: darkMode, set: setDarkMode, title: 'Dark mode' }, { value: darkMode, set: setDarkMode, title: 'Dark mode' },
{ value: showRouteActions, set: setShowRouteActions, title: 'Show route actions' },
{ value: showScreenshot, set: setShowScreenshot, title: 'Show screenshot instead of snapshot' }, { value: showScreenshot, set: setShowScreenshot, title: 'Show screenshot instead of snapshot' },
]} />} ]} />}
</div> </div>

View file

@ -24,7 +24,6 @@ import type { ErrorDescription } from './errorsTab';
import type { ConsoleEntry } from './consoleTab'; import type { ConsoleEntry } from './consoleTab';
import { ConsoleTab, useConsoleTabModel } from './consoleTab'; import { ConsoleTab, useConsoleTabModel } from './consoleTab';
import type * as modelUtil from './modelUtil'; import type * as modelUtil from './modelUtil';
import { isRouteAction } from './modelUtil';
import { NetworkTab, useNetworkTabModel } from './networkTab'; import { NetworkTab, useNetworkTabModel } from './networkTab';
import { SnapshotTab } from './snapshotTab'; import { SnapshotTab } from './snapshotTab';
import { SourceTab } from './sourceTab'; import { SourceTab } from './sourceTab';
@ -50,6 +49,7 @@ export const Workbench: React.FunctionComponent<{
rootDir?: string, rootDir?: string,
fallbackLocation?: modelUtil.SourceLocation, fallbackLocation?: modelUtil.SourceLocation,
isLive?: boolean, isLive?: boolean,
hideTimeline?: boolean,
status?: UITestStatus, status?: UITestStatus,
annotations?: { type: string; description?: string; }[]; annotations?: { type: string; description?: string; }[];
inert?: boolean, inert?: boolean,
@ -57,7 +57,7 @@ export const Workbench: React.FunctionComponent<{
onOpenExternally?: (location: modelUtil.SourceLocation) => void, onOpenExternally?: (location: modelUtil.SourceLocation) => void,
revealSource?: boolean, revealSource?: boolean,
showSettings?: boolean, showSettings?: boolean,
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, status, annotations, inert, openPage, onOpenExternally, revealSource, showSettings }) => { }> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, openPage, onOpenExternally, revealSource, showSettings }) => {
const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined); const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined);
const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined); const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined);
@ -70,13 +70,8 @@ export const Workbench: React.FunctionComponent<{
const [highlightedLocator, setHighlightedLocator] = React.useState<string>(''); const [highlightedLocator, setHighlightedLocator] = React.useState<string>('');
const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>(); const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>();
const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom'); const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom');
const [showRouteActions, setShowRouteActions] = useSetting('show-route-actions', true);
const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false); const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false);
const filteredActions = React.useMemo(() => {
return (model?.actions || []).filter(action => showRouteActions || !isRouteAction(action));
}, [model, showRouteActions]);
const setSelectedAction = React.useCallback((action: modelUtil.ActionTraceEventInContext | undefined) => { const setSelectedAction = React.useCallback((action: modelUtil.ActionTraceEventInContext | undefined) => {
setSelectedCallId(action?.callId); setSelectedCallId(action?.callId);
setRevealedError(undefined); setRevealedError(undefined);
@ -291,7 +286,7 @@ export const Workbench: React.FunctionComponent<{
</div>} </div>}
<ActionList <ActionList
sdkLanguage={sdkLanguage} sdkLanguage={sdkLanguage}
actions={filteredActions} actions={model?.actions || []}
selectedAction={model ? selectedAction : undefined} selectedAction={model ? selectedAction : undefined}
selectedTime={selectedTime} selectedTime={selectedTime}
setSelectedTime={setSelectedTime} setSelectedTime={setSelectedTime}
@ -311,13 +306,12 @@ export const Workbench: React.FunctionComponent<{
id: 'settings', id: 'settings',
title: 'Settings', title: 'Settings',
component: <SettingsView settings={[ component: <SettingsView settings={[
{ value: showRouteActions, set: setShowRouteActions, title: 'Show route actions' },
{ value: showScreenshot, set: setShowScreenshot, title: 'Show screenshot instead of snapshot' } { value: showScreenshot, set: setShowScreenshot, title: 'Show screenshot instead of snapshot' }
]}/>, ]}/>,
}; };
return <div className='vbox workbench' {...(inert ? { inert: 'true' } : {})}> return <div className='vbox workbench' {...(inert ? { inert: 'true' } : {})}>
<Timeline {!hideTimeline && <Timeline
model={model} model={model}
consoleEntries={consoleModel.entries} consoleEntries={consoleModel.entries}
boundaries={boundaries} boundaries={boundaries}
@ -328,7 +322,7 @@ export const Workbench: React.FunctionComponent<{
sdkLanguage={sdkLanguage} sdkLanguage={sdkLanguage}
selectedTime={selectedTime} selectedTime={selectedTime}
setSelectedTime={setSelectedTime} setSelectedTime={setSelectedTime}
/> />}
<SplitView <SplitView
sidebarSize={250} sidebarSize={250}
orientation={sidebarLocation === 'bottom' ? 'vertical' : 'horizontal'} settingName='propertiesSidebar' orientation={sidebarLocation === 'bottom' ? 'vertical' : 'horizontal'} settingName='propertiesSidebar'

View file

@ -17,6 +17,7 @@
import './toolbarButton.css'; import './toolbarButton.css';
import '../third_party/vscode/codicon.css'; import '../third_party/vscode/codicon.css';
import * as React from 'react'; import * as React from 'react';
import { clsx } from '@web/uiUtils';
export interface ToolbarButtonProps { export interface ToolbarButtonProps {
title: string, title: string,
@ -40,11 +41,8 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
testId, testId,
className, className,
}) => { }) => {
className = (className || '') + ` toolbar-button ${icon}`;
if (toggled)
className += ' toggled';
return <button return <button
className={className} className={clsx(className, 'toolbar-button', icon, toggled && 'toggled')}
onMouseDown={preventDefault} onMouseDown={preventDefault}
onClick={onClick} onClick={onClick}
onDoubleClick={preventDefault} onDoubleClick={preventDefault}

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -69,12 +69,16 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
await run(false); await run(false);
}, { scope: 'worker' }], }, { scope: 'worker' }],
defaultSameSiteCookieValue: [async ({ browserName, isLinux }, run) => { defaultSameSiteCookieValue: [async ({ browserName, platform }, run) => {
if (browserName === 'chromium' || browserName as any === '_bidiChromium') if (browserName === 'chromium' || browserName as any === '_bidiChromium')
await run('Lax'); await run('Lax');
else if (browserName === 'webkit' && isLinux) else if (browserName === 'webkit' && platform === 'linux')
await run('Lax'); await run('Lax');
else if (browserName === 'webkit' && !isLinux) else if (browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) >= 24)
// macOS 15 Sequoia onwards
await run('Lax');
else if (browserName === 'webkit')
// Windows + older macOS
await run('None'); await run('None');
else if (browserName === 'firefox' || browserName as any === '_bidiFirefox') else if (browserName === 'firefox' || browserName as any === '_bidiFirefox')
await run('None'); await run('None');

View file

@ -57,14 +57,14 @@ export class TestProxy {
this._prependHandler('request', (req: IncomingMessage) => { this._prependHandler('request', (req: IncomingMessage) => {
this.requestUrls.push(req.url); this.requestUrls.push(req.url);
const url = new URL(req.url); const url = new URL(req.url);
url.host = `localhost:${port}`; url.host = `127.0.0.1:${port}`;
req.url = url.toString(); req.url = url.toString();
}); });
this._prependHandler('connect', (req: IncomingMessage) => { this._prependHandler('connect', (req: IncomingMessage) => {
if (!options?.allowConnectRequests) if (!options?.allowConnectRequests)
return; return;
this.connectHosts.push(req.url); this.connectHosts.push(req.url);
req.url = `localhost:${port}`; req.url = `127.0.0.1:${port}`;
}); });
} }
@ -138,10 +138,10 @@ export async function setupSocksForwardingServer({
connections.get(payload.uid)?.destroy(); connections.get(payload.uid)?.destroy();
connections.delete(payload.uid); connections.delete(payload.uid);
}); });
await socksProxy.listen(port, 'localhost'); await socksProxy.listen(port, '127.0.0.1');
return { return {
closeProxyServer: () => socksProxy.close(), closeProxyServer: () => socksProxy.close(),
proxyServerAddr: `socks5://localhost:${port}`, proxyServerAddr: `socks5://127.0.0.1:${port}`,
connectHosts, connectHosts,
}; };
} }

View file

@ -21,6 +21,7 @@ import * as playwrightLibrary from 'playwright-core';
export type TestModeWorkerOptions = { export type TestModeWorkerOptions = {
mode: TestModeName; mode: TestModeName;
codegenMode: 'trace-events' | 'actions';
}; };
export type TestModeTestFixtures = { export type TestModeTestFixtures = {
@ -48,6 +49,7 @@ export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOpti
await run(playwright); await run(playwright);
await testMode.teardown(); await testMode.teardown();
}, { scope: 'worker' }], }, { scope: 'worker' }],
codegenMode: ['actions', { scope: 'worker', option: true }],
toImplInWorkerScope: [async ({ playwright }, use) => { toImplInWorkerScope: [async ({ playwright }, use) => {
await use((playwright as any)._toImpl); await use((playwright as any)._toImpl);

View file

@ -22,6 +22,7 @@ import type net from 'net';
import path from 'path'; import path from 'path';
import url from 'url'; import url from 'url';
import util from 'util'; import util from 'util';
import type stream from 'stream';
import ws from 'ws'; import ws from 'ws';
import zlib, { gzip } from 'zlib'; import zlib, { gzip } from 'zlib';
import { createHttpServer, createHttpsServer } from '../../../packages/playwright-core/lib/utils/network'; import { createHttpServer, createHttpsServer } from '../../../packages/playwright-core/lib/utils/network';
@ -31,6 +32,11 @@ const rejectSymbol = Symbol('reject callback');
const gzipAsync = util.promisify(gzip.bind(zlib)); const gzipAsync = util.promisify(gzip.bind(zlib));
type UpgradeActions = {
doUpgrade: () => void;
socket: stream.Duplex;
};
export class TestServer { export class TestServer {
private _server: http.Server; private _server: http.Server;
private _wsServer: ws.WebSocketServer; private _wsServer: ws.WebSocketServer;
@ -44,6 +50,7 @@ export class TestServer {
private _extraHeaders = new Map<string, object>(); private _extraHeaders = new Map<string, object>();
private _gzipRoutes = new Set<string>(); private _gzipRoutes = new Set<string>();
private _requestSubscribers = new Map<string, Promise<any>>(); private _requestSubscribers = new Map<string, Promise<any>>();
private _upgradeCallback: (actions: UpgradeActions) => void | undefined;
readonly PORT: number; readonly PORT: number;
readonly PREFIX: string; readonly PREFIX: string;
readonly CROSS_PROCESS_PREFIX: string; readonly CROSS_PROCESS_PREFIX: string;
@ -73,6 +80,16 @@ export class TestServer {
this._server.on('connection', socket => this._onSocket(socket)); this._server.on('connection', socket => this._onSocket(socket));
this._wsServer = new ws.WebSocketServer({ noServer: true }); this._wsServer = new ws.WebSocketServer({ noServer: true });
this._server.on('upgrade', async (request, socket, head) => { this._server.on('upgrade', async (request, socket, head) => {
const doUpgrade = () => {
this._wsServer.handleUpgrade(request, socket, head, ws => {
// Next emit is only for our internal 'connection' listeners.
this._wsServer.emit('connection', ws, request);
});
};
if (this._upgradeCallback) {
this._upgradeCallback({ doUpgrade, socket });
return;
}
const pathname = url.parse(request.url!).path; const pathname = url.parse(request.url!).path;
if (pathname === '/ws-401') { if (pathname === '/ws-401') {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\nUnauthorized body'); socket.write('HTTP/1.1 401 Unauthorized\r\n\r\nUnauthorized body');
@ -86,10 +103,7 @@ export class TestServer {
socket.destroy(); socket.destroy();
return; return;
} }
this._wsServer.handleUpgrade(request, socket, head, ws => { doUpgrade();
// Next emit is only for our internal 'connection' listeners.
this._wsServer.emit('connection', ws, request);
});
}); });
this._server.listen(port); this._server.listen(port);
this._dirPath = dirPath; this._dirPath = dirPath;
@ -177,6 +191,8 @@ export class TestServer {
this._csp.clear(); this._csp.clear();
this._extraHeaders.clear(); this._extraHeaders.clear();
this._gzipRoutes.clear(); this._gzipRoutes.clear();
this._upgradeCallback = undefined;
this._wsServer.removeAllListeners('connection');
this._server.closeAllConnections(); this._server.closeAllConnections();
const error = new Error('Static Server has been reset'); const error = new Error('Static Server has been reset');
for (const subscriber of this._requestSubscribers.values()) for (const subscriber of this._requestSubscribers.values())
@ -294,6 +310,14 @@ export class TestServer {
}); });
} }
waitForUpgrade() {
return new Promise<UpgradeActions>(fulfill => this._upgradeCallback = fulfill);
}
waitForWebSocket() {
return new Promise<ws.WebSocket>(fulfill => this._wsServer.once('connection', (ws, req) => fulfill(ws)));
}
sendOnWebSocketConnection(data) { sendOnWebSocketConnection(data) {
this.onceWebSocketConnection(ws => ws.send(data)); this.onceWebSocketConnection(ws => ws.send(data));
} }

View file

@ -71,10 +71,12 @@ class TraceViewerPage {
return await this.page.waitForSelector(`.list-view-entry:has-text("${action}") .action-icons`); return await this.page.waitForSelector(`.list-view-entry:has-text("${action}") .action-icons`);
} }
@step
async selectAction(title: string, ordinal: number = 0) { async selectAction(title: string, ordinal: number = 0) {
await this.page.locator(`.action-title:has-text("${title}")`).nth(ordinal).click(); await this.page.locator(`.action-title:has-text("${title}")`).nth(ordinal).click();
} }
@step
async selectSnapshot(name: string) { async selectSnapshot(name: string) {
await this.page.click(`.snapshot-tab .tabbed-pane-tab-label:has-text("${name}")`); await this.page.click(`.snapshot-tab .tabbed-pane-tab-label:has-text("${name}")`);
} }

View file

@ -160,7 +160,7 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: (EventTraceEvent | ConsoleMessageTraceEvent)[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[], errors: string[] }> { export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: (EventTraceEvent | ConsoleMessageTraceEvent)[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[], errors: string[] }> {
const backend = new TraceBackend(file); const backend = new TraceBackend(file);
const traceModel = new TraceModel(); const traceModel = new TraceModel();
await traceModel.load(backend, () => {}); await traceModel.load(backend, false, () => {});
const model = new MultiTraceModel(traceModel.contextEntries); const model = new MultiTraceModel(traceModel.contextEntries);
const { rootItem } = buildActionTree(model.actions); const { rootItem } = buildActionTree(model.actions);
const actionTree: string[] = []; const actionTree: string[] = [];

View file

@ -15,6 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import os from 'os';
import { contextTest as it, expect } from '../config/browserTest'; import { contextTest as it, expect } from '../config/browserTest';
it('should return no cookies in pristine browser context', async ({ context, page, server }) => { it('should return no cookies in pristine browser context', async ({ context, page, server }) => {
@ -396,7 +397,7 @@ it('should support requestStorageAccess', async ({ page, server, channel, browse
server.waitForRequest('/title.html'), server.waitForRequest('/title.html'),
frame.evaluate(() => fetch('/title.html')) frame.evaluate(() => fetch('/title.html'))
]); ]);
if (isLinux && browserName === 'webkit') if ((isLinux || (isMac && parseInt(os.release(), 10) >= 24)) && browserName === 'webkit')
expect(serverRequest.headers.cookie).toBe(undefined); expect(serverRequest.headers.cookie).toBe(undefined);
else else
expect(serverRequest.headers.cookie).toBe('name=value'); expect(serverRequest.headers.cookie).toBe('name=value');

View file

@ -15,6 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { attachFrame } from 'tests/config/utils';
import { browserTest as it, expect } from '../config/browserTest'; import { browserTest as it, expect } from '../config/browserTest';
import fs from 'fs'; import fs from 'fs';
@ -275,3 +276,43 @@ it('should work when service worker is intefering', async ({ page, context, serv
const storageState = await context.storageState(); const storageState = await context.storageState();
expect(storageState.origins[0].localStorage[0]).toEqual({ name: 'foo', value: 'bar' }); expect(storageState.origins[0].localStorage[0]).toEqual({ name: 'foo', value: 'bar' });
}); });
it('should set local storage in third-party context', async ({ contextFactory, server }) => {
const context = await contextFactory({
storageState: {
cookies: [],
origins: [
{
origin: server.CROSS_PROCESS_PREFIX,
localStorage: [{
name: 'name1',
value: 'value1'
}]
},
]
}
});
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
const frame = await attachFrame(page, 'frame1', server.CROSS_PROCESS_PREFIX + '/empty.html');
const localStorage = await frame.evaluate('window.localStorage');
expect(localStorage).toEqual({ name1: 'value1' });
await context.close();
});
it('should roundtrip local storage in third-party context', async ({ page, contextFactory, server }) => {
await page.goto(server.EMPTY_PAGE);
const frame = await attachFrame(page, 'frame1', server.CROSS_PROCESS_PREFIX + '/empty.html');
await frame.evaluate(() => window.localStorage.setItem('name1', 'value1'));
const storageState = await page.context().storageState();
const context2 = await contextFactory({ storageState });
const page2 = await context2.newPage();
await page2.goto(server.EMPTY_PAGE);
const frame2 = await attachFrame(page2, 'frame1', server.CROSS_PROCESS_PREFIX + '/empty.html');
const localStorage = await frame2.evaluate('window.localStorage');
expect(localStorage).toEqual({ name1: 'value1' });
await context2.close();
});

View file

@ -28,7 +28,6 @@ const { createHttpsServer, createHttp2Server } = require('../../packages/playwri
type TestOptions = { type TestOptions = {
startCCServer(options?: { startCCServer(options?: {
host?: string;
http2?: boolean; http2?: boolean;
enableHTTP1FallbackWhenUsingHttp2?: boolean; enableHTTP1FallbackWhenUsingHttp2?: boolean;
useFakeLocalhost?: boolean; useFakeLocalhost?: boolean;
@ -68,8 +67,8 @@ const test = base.extend<TestOptions>({
} }
res.end(parts.map(({ key, value }) => `<div data-testid="${key}">${value}</div>`).join('')); res.end(parts.map(({ key, value }) => `<div data-testid="${key}">${value}</div>`).join(''));
}); });
await new Promise<void>(f => server.listen(0, options?.host ?? 'localhost', () => f())); await new Promise<void>(f => server.listen(0, '127.0.0.1', () => f()));
const host = options?.useFakeLocalhost ? 'local.playwright' : 'localhost'; const host = options?.useFakeLocalhost ? 'local.playwright' : '127.0.0.1';
return `https://${host}:${(server.address() as net.AddressInfo).port}/`; return `https://${host}:${(server.address() as net.AddressInfo).port}/`;
}); });
if (server) if (server)
@ -195,7 +194,7 @@ test.describe('fetch', () => {
}); });
test('pass with trusted client certificates and when a socks proxy is used', async ({ playwright, startCCServer, asset }) => { test('pass with trusted client certificates and when a socks proxy is used', async ({ playwright, startCCServer, asset }) => {
const serverURL = await startCCServer({ host: '127.0.0.1' }); const serverURL = await startCCServer();
const serverPort = parseInt(new URL(serverURL).port, 10); const serverPort = parseInt(new URL(serverURL).port, 10);
const { proxyServerAddr, closeProxyServer, connectHosts } = await setupSocksForwardingServer({ const { proxyServerAddr, closeProxyServer, connectHosts } = await setupSocksForwardingServer({
port: test.info().workerIndex + 2048 + 2, port: test.info().workerIndex + 2048 + 2,
@ -366,13 +365,14 @@ test.describe('browser', () => {
}); });
expect(proxyServer.connectHosts).toEqual([]); expect(proxyServer.connectHosts).toEqual([]);
await page.goto(serverURL); await page.goto(serverURL);
expect([...new Set(proxyServer.connectHosts)]).toEqual([`localhost:${new URL(serverURL).port}`]); const host = browserName === 'webkit' && process.platform === 'darwin' ? 'localhost' : '127.0.0.1';
expect([...new Set(proxyServer.connectHosts)]).toEqual([`${host}:${new URL(serverURL).port}`]);
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!'); await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
await page.close(); await page.close();
}); });
test('should pass with matching certificates and when a socks proxy is used', async ({ browser, startCCServer, asset, browserName }) => { test('should pass with matching certificates and when a socks proxy is used', async ({ browser, startCCServer, asset, browserName }) => {
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin', host: '127.0.0.1' }); const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
const serverPort = parseInt(new URL(serverURL).port, 10); const serverPort = parseInt(new URL(serverURL).port, 10);
const { proxyServerAddr, closeProxyServer, connectHosts } = await setupSocksForwardingServer({ const { proxyServerAddr, closeProxyServer, connectHosts } = await setupSocksForwardingServer({
port: test.info().workerIndex + 2048 + 2, port: test.info().workerIndex + 2048 + 2,
@ -390,7 +390,8 @@ test.describe('browser', () => {
}); });
expect(connectHosts).toEqual([]); expect(connectHosts).toEqual([]);
await page.goto(serverURL); await page.goto(serverURL);
expect(connectHosts).toEqual([`localhost:${serverPort}`]); const host = browserName === 'webkit' && process.platform === 'darwin' ? 'localhost' : '127.0.0.1';
expect(connectHosts).toEqual([`${host}:${serverPort}`]);
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!'); await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
await page.close(); await page.close();
await closeProxyServer(); await closeProxyServer();
@ -625,7 +626,7 @@ test.describe('browser', () => {
}); });
test('should pass with matching certificates on context APIRequestContext instance', async ({ browser, startCCServer, asset, browserName }) => { test('should pass with matching certificates on context APIRequestContext instance', async ({ browser, startCCServer, asset, browserName }) => {
const serverURL = await startCCServer({ host: '127.0.0.1' }); const serverURL = await startCCServer();
const baseOptions = { const baseOptions = {
certPath: asset('client-certificates/client/trusted/cert.pem'), certPath: asset('client-certificates/client/trusted/cert.pem'),
keyPath: asset('client-certificates/client/trusted/key.pem'), keyPath: asset('client-certificates/client/trusted/key.pem'),
@ -688,7 +689,7 @@ test.describe('browser', () => {
}], }],
}); });
{ {
await page.goto(serverURL.replace('localhost', 'local.playwright')); await page.goto(serverURL.replace('127.0.0.1', 'local.playwright'));
await expect(page.getByTestId('message')).toHaveText('Sorry, but you need to provide a client certificate to continue.'); await expect(page.getByTestId('message')).toHaveText('Sorry, but you need to provide a client certificate to continue.');
await expect(page.getByTestId('alpn-protocol')).toHaveText('h2'); await expect(page.getByTestId('alpn-protocol')).toHaveText('h2');
await expect(page.getByTestId('servername')).toHaveText('local.playwright'); await expect(page.getByTestId('servername')).toHaveText('local.playwright');
@ -714,7 +715,7 @@ test.describe('browser', () => {
}], }],
}); });
{ {
await page.goto(serverURL.replace('localhost', 'local.playwright')); await page.goto(serverURL.replace('127.0.0.1', 'local.playwright'));
await expect(page.getByTestId('message')).toHaveText('Sorry, but you need to provide a client certificate to continue.'); await expect(page.getByTestId('message')).toHaveText('Sorry, but you need to provide a client certificate to continue.');
await expect(page.getByTestId('alpn-protocol')).toHaveText('http/1.1'); await expect(page.getByTestId('alpn-protocol')).toHaveText('http/1.1');
} }

View file

@ -610,6 +610,7 @@ it('should have security details', async ({ contextFactory, httpsServer, browser
const { page, getLog } = await pageWithHar(contextFactory, testInfo); const { page, getLog } = await pageWithHar(contextFactory, testInfo);
await page.goto(httpsServer.EMPTY_PAGE); await page.goto(httpsServer.EMPTY_PAGE);
await page.request.get(httpsServer.EMPTY_PAGE);
const log = await getLog(); const log = await getLog();
const { serverIPAddress, _serverPort: port, _securityDetails: securityDetails } = log.entries[0]; const { serverIPAddress, _serverPort: port, _securityDetails: securityDetails } = log.entries[0];
if (!mode.startsWith('service')) { if (!mode.startsWith('service')) {
@ -620,6 +621,8 @@ it('should have security details', async ({ contextFactory, httpsServer, browser
expect(securityDetails).toEqual({ protocol: 'TLS 1.3', subjectName: 'playwright-test', validFrom: 1691708270, validTo: 2007068270 }); expect(securityDetails).toEqual({ protocol: 'TLS 1.3', subjectName: 'playwright-test', validFrom: 1691708270, validTo: 2007068270 });
else else
expect(securityDetails).toEqual({ issuer: 'playwright-test', protocol: 'TLS 1.3', subjectName: 'playwright-test', validFrom: 1691708270, validTo: 2007068270 }); expect(securityDetails).toEqual({ issuer: 'playwright-test', protocol: 'TLS 1.3', subjectName: 'playwright-test', validFrom: 1691708270, validTo: 2007068270 });
expect(log.entries[1]._securityDetails).toEqual({ issuer: 'playwright-test', protocol: 'TLSv1.3', subjectName: 'playwright-test', validFrom: 1691708270, validTo: 2007068270 });
}); });
it('should have connection details for redirects', async ({ contextFactory, server, browserName, mode }, testInfo) => { it('should have connection details for redirects', async ({ contextFactory, server, browserName, mode }, testInfo) => {
@ -820,6 +823,7 @@ it('should include API request', async ({ contextFactory, server }, testInfo) =>
expect(entry.response.headers.find(h => h.name.toLowerCase() === 'content-type')?.value).toContain('application/json'); expect(entry.response.headers.find(h => h.name.toLowerCase() === 'content-type')?.value).toContain('application/json');
expect(entry.response.content.size).toBe(15); expect(entry.response.content.size).toBe(15);
expect(entry.response.content.text).toBe(responseBody.toString()); expect(entry.response.content.text).toBe(responseBody.toString());
expect(entry.response.bodySize).toBe(15);
expect(entry.time).toBeGreaterThan(0); expect(entry.time).toBeGreaterThan(0);
expect(entry.timings).toEqual(expect.objectContaining({ expect(entry.timings).toEqual(expect.objectContaining({
@ -831,6 +835,9 @@ it('should include API request', async ({ contextFactory, server }, testInfo) =>
ssl: expect.any(Number), ssl: expect.any(Number),
wait: expect.any(Number), wait: expect.any(Number),
})); }));
expect(entry.serverIPAddress).toBeDefined();
expect(entry._serverPort).toEqual(server.PORT);
}); });
it('should respect minimal mode for API Requests', async ({ contextFactory, server }, testInfo) => { it('should respect minimal mode for API Requests', async ({ contextFactory, server }, testInfo) => {
@ -844,6 +851,11 @@ it('should respect minimal mode for API Requests', async ({ contextFactory, serv
expect(entries).toHaveLength(1); expect(entries).toHaveLength(1);
const [entry] = entries; const [entry] = entries;
expect(entry.timings).toEqual({ receive: -1, send: -1, wait: -1 }); expect(entry.timings).toEqual({ receive: -1, send: -1, wait: -1 });
expect(entry.serverIPAddress).toBeUndefined();
expect(entry._serverPort).toBeUndefined();
expect(entry.request.cookies).toEqual([]);
expect(entry.request.bodySize).toBe(-1);
expect(entry.response.bodySize).toBe(-1);
}); });
it('should include redirects from API request', async ({ contextFactory, server }, testInfo) => { it('should include redirects from API request', async ({ contextFactory, server }, testInfo) => {

View file

@ -20,8 +20,8 @@ import type { ConsoleMessage } from 'playwright';
test.describe('cli codegen', () => { test.describe('cli codegen', () => {
test.skip(({ mode }) => mode !== 'default'); test.skip(({ mode }) => mode !== 'default');
test('should click', async ({ page, openRecorder }) => { test('should click', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<button onclick="console.log('click')">Submit</button>`); await recorder.setContentAndWait(`<button onclick="console.log('click')">Submit</button>`);
@ -52,8 +52,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`)
expect(message.text()).toBe('click'); expect(message.text()).toBe('click');
}); });
test('should double click', async ({ page, openRecorder }) => { test('should double click', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<button onclick="console.log('click ' + event.detail)" ondblclick="console.log('dblclick ' + event.detail)">Submit</button>`); await recorder.setContentAndWait(`<button onclick="console.log('click ' + event.detail)" ondblclick="console.log('dblclick ' + event.detail)">Submit</button>`);
@ -93,8 +93,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).DblClickAsync()
]); ]);
}); });
test('should ignore programmatic events', async ({ page, openRecorder }) => { test('should ignore programmatic events', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<button onclick="console.log('click')">Submit</button>`); await recorder.setContentAndWait(`<button onclick="console.log('click')">Submit</button>`);
@ -113,8 +113,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).DblClickAsync()
expect(clicks.length).toBe(1); expect(clicks.length).toBe(1);
}); });
test('should click after same-document navigation', async ({ page, openRecorder, server }) => { test('should click after same-document navigation', async ({ openRecorder, server }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
server.setRoute('/foo.html', (req, res) => { server.setRoute('/foo.html', (req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('Content-Type', 'text/html; charset=utf-8');
@ -143,8 +143,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).DblClickAsync()
expect(message.text()).toBe('click'); expect(message.text()).toBe('click');
}); });
test('should make a positioned click on a canvas', async ({ page, openRecorder }) => { test('should make a positioned click on a canvas', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<canvas width="500" height="500" style="margin: 42px"/> <canvas width="500" height="500" style="margin: 42px"/>
@ -196,8 +196,8 @@ await page.Locator("canvas").ClickAsync(new LocatorClickOptions
expect(message.text()).toBe('click 250 250'); expect(message.text()).toBe('click 250 250');
}); });
test('should work with TrustedTypes', async ({ page, openRecorder }) => { test('should work with TrustedTypes', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<head> <head>
@ -234,8 +234,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`)
expect(message.text()).toBe('click'); expect(message.text()).toBe('click');
}); });
test('should not target selector preview by text regexp', async ({ page, openRecorder }) => { test('should not target selector preview by text regexp', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<span>dummy</span>`); await recorder.setContentAndWait(`<span>dummy</span>`);
@ -265,8 +265,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`)
expect(message.text()).toBe('click'); expect(message.text()).toBe('click');
}); });
test('should fill', async ({ page, openRecorder }) => { test('should fill', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input id="input" name="name" oninput="console.log(input.value)"></input>`); await recorder.setContentAndWait(`<input id="input" name="name" oninput="console.log(input.value)"></input>`);
const locator = await recorder.focusElement('input'); const locator = await recorder.focusElement('input');
@ -295,8 +295,8 @@ await page.Locator("#input").FillAsync(\"John\");`);
expect(message.text()).toBe('John'); expect(message.text()).toBe('John');
}); });
test('should fill japanese text', async ({ page, openRecorder }) => { test('should fill japanese text', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
// In Japanese, "てすと" or "テスト" means "test". // In Japanese, "てすと" or "テスト" means "test".
await recorder.setContentAndWait(`<input id="input" name="name" oninput="input.value === 'てすと' && console.log(input.value)"></input>`); await recorder.setContentAndWait(`<input id="input" name="name" oninput="input.value === 'てすと' && console.log(input.value)"></input>`);
@ -329,8 +329,8 @@ await page.Locator("#input").FillAsync(\"てすと\");`);
expect(message.text()).toBe('てすと'); expect(message.text()).toBe('てすと');
}); });
test('should fill textarea', async ({ page, openRecorder }) => { test('should fill textarea', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<textarea id="textarea" name="name" oninput="console.log(textarea.value)"></textarea>`); await recorder.setContentAndWait(`<textarea id="textarea" name="name" oninput="console.log(textarea.value)"></textarea>`);
const locator = await recorder.focusElement('textarea'); const locator = await recorder.focusElement('textarea');
@ -346,9 +346,9 @@ await page.Locator("#input").FillAsync(\"てすと\");`);
expect(message.text()).toBe('John'); expect(message.text()).toBe('John');
}); });
test('should fill textarea with new lines at the end', async ({ page, openRecorder }) => { test('should fill textarea with new lines at the end', async ({ openRecorder }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23774' }); test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23774' });
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<textarea id="textarea"></textarea>`); await recorder.setContentAndWait(`<textarea id="textarea"></textarea>`);
const textarea = page.locator('textarea'); const textarea = page.locator('textarea');
await textarea.evaluate<void, HTMLTextAreaElement>(e => e.addEventListener('input', () => (window as any).lastInputValue = e.value)); await textarea.evaluate<void, HTMLTextAreaElement>(e => e.addEventListener('input', () => (window as any).lastInputValue = e.value));
@ -361,8 +361,8 @@ await page.Locator("#input").FillAsync(\"てすと\");`);
expect(sources.get('JavaScript')!.text).not.toContain(`Enter`); expect(sources.get('JavaScript')!.text).not.toContain(`Enter`);
}); });
test('should fill [contentEditable]', async ({ page, openRecorder }) => { test('should fill [contentEditable]', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<div id="content" contenteditable="" oninput="console.log(content.innerText)"/>`); await recorder.setContentAndWait(`<div id="content" contenteditable="" oninput="console.log(content.innerText)"/>`);
const locator = await recorder.focusElement('div'); const locator = await recorder.focusElement('div');
@ -378,8 +378,8 @@ await page.Locator("#input").FillAsync(\"てすと\");`);
expect(message.text()).toBe('John Doe'); expect(message.text()).toBe('John Doe');
}); });
test('should press', async ({ page, openRecorder }) => { test('should press', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input name="name" onkeypress="console.log('press')"></input>`); await recorder.setContentAndWait(`<input name="name" onkeypress="console.log('press')"></input>`);
@ -412,8 +412,8 @@ await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`);
expect(messages[0].text()).toBe('press'); expect(messages[0].text()).toBe('press');
}); });
test('should update selected element after pressing Tab', async ({ page, openRecorder }) => { test('should update selected element after pressing Tab', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<input name="one"></input> <input name="one"></input>
@ -441,8 +441,8 @@ await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`);
await page.locator('input[name="two"]').fill('barfoo321');`); await page.locator('input[name="two"]').fill('barfoo321');`);
}); });
test('should record ArrowDown', async ({ page, openRecorder }) => { test('should record ArrowDown', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input name="name" onkeydown="console.log('press:' + event.key)"></input>`); await recorder.setContentAndWait(`<input name="name" onkeydown="console.log('press:' + event.key)"></input>`);
@ -463,8 +463,8 @@ await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`);
expect(messages[0].text()).toBe('press:ArrowDown'); expect(messages[0].text()).toBe('press:ArrowDown');
}); });
test('should emit single keyup on ArrowDown', async ({ page, openRecorder }) => { test('should emit single keyup on ArrowDown', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input name="name" onkeydown="console.log('down:' + event.key)" onkeyup="console.log('up:' + event.key)"></input>`); await recorder.setContentAndWait(`<input name="name" onkeydown="console.log('down:' + event.key)" onkeyup="console.log('up:' + event.key)"></input>`);
@ -488,8 +488,8 @@ await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`);
expect(messages[1].text()).toBe('up:ArrowDown'); expect(messages[1].text()).toBe('up:ArrowDown');
}); });
test('should check', async ({ page, openRecorder }) => { test('should check', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="console.log(checkbox.checked)"></input>`); await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="console.log(checkbox.checked)"></input>`);
@ -520,8 +520,8 @@ await page.Locator("#checkbox").CheckAsync();`);
expect(message.text()).toBe('true'); expect(message.text()).toBe('true');
}); });
test('should check a radio button', async ({ page, openRecorder }) => { test('should check a radio button', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input id="checkbox" type="radio" name="accept" onchange="console.log(checkbox.checked)"></input>`); await recorder.setContentAndWait(`<input id="checkbox" type="radio" name="accept" onchange="console.log(checkbox.checked)"></input>`);
@ -539,8 +539,8 @@ await page.Locator("#checkbox").CheckAsync();`);
expect(message.text()).toBe('true'); expect(message.text()).toBe('true');
}); });
test('should check with keyboard', async ({ page, openRecorder }) => { test('should check with keyboard', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="console.log(checkbox.checked)"></input>`); await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="console.log(checkbox.checked)"></input>`);
@ -558,8 +558,8 @@ await page.Locator("#checkbox").CheckAsync();`);
expect(message.text()).toBe('true'); expect(message.text()).toBe('true');
}); });
test('should uncheck', async ({ page, openRecorder }) => { test('should uncheck', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" checked name="accept" onchange="console.log(checkbox.checked)"></input>`); await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" checked name="accept" onchange="console.log(checkbox.checked)"></input>`);
@ -590,8 +590,8 @@ await page.Locator("#checkbox").UncheckAsync();`);
expect(message.text()).toBe('false'); expect(message.text()).toBe('false');
}); });
test('should select', async ({ page, openRecorder }) => { test('should select', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait('<select id="age" onchange="console.log(age.selectedOptions[0].value)"><option value="1"><option value="2"></select>'); await recorder.setContentAndWait('<select id="age" onchange="console.log(age.selectedOptions[0].value)"><option value="1"><option value="2"></select>');
@ -623,8 +623,8 @@ await page.Locator("#age").SelectOptionAsync(new[] { "2" });`);
expect(message.text()).toBe('2'); expect(message.text()).toBe('2');
}); });
test('should select with size attribute', async ({ page, openRecorder }) => { test('should select with size attribute', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<style> <style>
@ -664,8 +664,8 @@ await page.Locator(\"#age\").SelectOptionAsync(new[] { \"2\" });`);
expect(message.text()).toBe('2'); expect(message.text()).toBe('2');
}); });
test('should await popup', async ({ page, openRecorder, browserName, headless }) => { test('should await popup', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait('<a target=_blank rel=noopener href="about:blank">link</a>'); await recorder.setContentAndWait('<a target=_blank rel=noopener href="about:blank">link</a>');
const locator = await recorder.hoverOverElement('a'); const locator = await recorder.hoverOverElement('a');
@ -706,8 +706,8 @@ var page1 = await page.RunAndWaitForPopupAsync(async () =>
expect(popup.url()).toBe('about:blank'); expect(popup.url()).toBe('about:blank');
}); });
test('should attribute navigation to click', async ({ page, openRecorder }) => { test('should attribute navigation to click', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<a onclick="window.location.href='about:blank#foo'">link</a>`); await recorder.setContentAndWait(`<a onclick="window.location.href='about:blank#foo'">link</a>`);
@ -763,7 +763,7 @@ await page.GetByText("link").ClickAsync();`);
test('should ignore AltGraph', async ({ openRecorder, browserName }) => { test('should ignore AltGraph', async ({ openRecorder, browserName }) => {
test.skip(browserName === 'firefox', 'The TextInputProcessor in Firefox does not work with AltGraph.'); test.skip(browserName === 'firefox', 'The TextInputProcessor in Firefox does not work with AltGraph.');
const recorder = await openRecorder(); const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<input></input>`); await recorder.setContentAndWait(`<input></input>`);
await recorder.page.type('input', 'playwright'); await recorder.page.type('input', 'playwright');
@ -775,8 +775,8 @@ await page.GetByText("link").ClickAsync();`);
expect(recorder.sources().get('JavaScript')!.text).toContain(`await page.getByRole('textbox').fill('playwright@example.com');`); expect(recorder.sources().get('JavaScript')!.text).toContain(`await page.getByRole('textbox').fill('playwright@example.com');`);
}); });
test('should middle click', async ({ page, openRecorder, server }) => { test('should middle click', async ({ openRecorder, server }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<a href${JSON.stringify(server.EMPTY_PAGE)}>Click me</a>`); await recorder.setContentAndWait(`<a href${JSON.stringify(server.EMPTY_PAGE)}>Click me</a>`);
@ -807,8 +807,8 @@ await page.GetByText("Click me").ClickAsync(new LocatorClickOptions
});`); });`);
}); });
test('should record slider', async ({ page, openRecorder }) => { test('should record slider', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input type="range" min="0" max="10" value="5">`); await recorder.setContentAndWait(`<input type="range" min="0" max="10" value="5">`);
@ -846,10 +846,10 @@ await page.GetByText("Click me").ClickAsync(new LocatorClickOptions
await page.GetByRole(AriaRole.Slider).FillAsync("10");`); await page.GetByRole(AriaRole.Slider).FillAsync("10");`);
}); });
test('should click button with nested div', async ({ page, openRecorder }) => { test('should click button with nested div', async ({ openRecorder }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29067' }); test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29067' });
const recorder = await openRecorder(); const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<button><div role="none">Submit</div></button>`); await recorder.setContentAndWait(`<button><div role="none">Submit</div></button>`);
@ -878,8 +878,8 @@ await page.GetByRole(AriaRole.Slider).FillAsync("10");`);
await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`); await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`);
}); });
test('should record omnibox navigations after performAction', async ({ page, openRecorder, server }) => { test('should record omnibox navigations after performAction', async ({ openRecorder, server }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<button>Submit</button>`); await recorder.setContentAndWait(`<button>Submit</button>`);
await Promise.all([ await Promise.all([
recorder.waitForOutput('JavaScript', 'click'), recorder.waitForOutput('JavaScript', 'click'),
@ -890,8 +890,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`)
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/empty.html');`); await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/empty.html');`);
}); });
test('should record omnibox navigations after recordAction', async ({ page, openRecorder, server }) => { test('should record omnibox navigations after recordAction', async ({ openRecorder, server }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<textarea></textarea>`); await recorder.setContentAndWait(`<textarea></textarea>`);
await Promise.all([ await Promise.all([
recorder.waitForOutput('JavaScript', 'fill'), recorder.waitForOutput('JavaScript', 'fill'),
@ -902,8 +902,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`)
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/empty.html');`); await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/empty.html');`);
}); });
test('should not throw csp directive violation errors', async ({ page, openRecorder, server }) => { test('should not throw csp directive violation errors', async ({ openRecorder, server }) => {
await openRecorder(); const { page } = await openRecorder();
await page.goto(server.PREFIX + '/csp.html'); await page.goto(server.PREFIX + '/csp.html');
const predicate = (msg: ConsoleMessage) => msg.type() === 'error' && /Content[\- ]Security[\- ]Policy/i.test(msg.text()); const predicate = (msg: ConsoleMessage) => msg.type() === 'error' && /Content[\- ]Security[\- ]Policy/i.test(msg.text());
await expect(page.waitForEvent('console', { predicate, timeout: 1000 })).rejects.toThrow(); await expect(page.waitForEvent('console', { predicate, timeout: 1000 })).rejects.toThrow();

View file

@ -22,7 +22,7 @@ test.describe('cli codegen', () => {
test.skip(({ mode }) => mode !== 'default'); test.skip(({ mode }) => mode !== 'default');
test('should contain open page', async ({ openRecorder }) => { test('should contain open page', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { recorder } = await openRecorder();
await recorder.setContentAndWait(``); await recorder.setContentAndWait(``);
const sources = await recorder.waitForOutput('JavaScript', `page.goto`); const sources = await recorder.waitForOutput('JavaScript', `page.goto`);
@ -43,8 +43,8 @@ test.describe('cli codegen', () => {
var page = await context.NewPageAsync();`); var page = await context.NewPageAsync();`);
}); });
test('should contain second page', async ({ openRecorder, page }) => { test('should contain second page', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(``); await recorder.setContentAndWait(``);
await page.context().newPage(); await page.context().newPage();
@ -66,8 +66,8 @@ var page = await context.NewPageAsync();`);
var page1 = await context.NewPageAsync();`); var page1 = await context.NewPageAsync();`);
}); });
test('should contain close page', async ({ openRecorder, page }) => { test('should contain close page', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(``); await recorder.setContentAndWait(``);
await page.context().newPage(); await page.context().newPage();
@ -90,8 +90,8 @@ var page1 = await context.NewPageAsync();`);
await page.CloseAsync();`); await page.CloseAsync();`);
}); });
test('should not lead to an error if html gets clicked', async ({ page, openRecorder }) => { test('should not lead to an error if html gets clicked', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(''); await recorder.setContentAndWait('');
await page.context().newPage(); await page.context().newPage();
@ -104,9 +104,9 @@ await page.CloseAsync();`);
expect(errors.length).toBe(0); expect(errors.length).toBe(0);
}); });
test('should upload a single file', async ({ page, openRecorder, browserName, asset, isLinux }) => { test('should upload a single file', async ({ openRecorder, browserName, asset, isLinux }) => {
test.fixme(browserName === 'firefox' && isLinux, 'https://bugzilla.mozilla.org/show_bug.cgi?id=1827551'); test.fixme(browserName === 'firefox' && isLinux, 'https://bugzilla.mozilla.org/show_bug.cgi?id=1827551');
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<form> <form>
<input type="file"> <input type="file">
@ -135,9 +135,9 @@ await page.CloseAsync();`);
await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-upload.txt\" });`); await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-upload.txt\" });`);
}); });
test('should upload multiple files', async ({ page, openRecorder, browserName, asset, isLinux }) => { test('should upload multiple files', async ({ openRecorder, browserName, asset, isLinux }) => {
test.fixme(browserName === 'firefox' && isLinux, 'https://bugzilla.mozilla.org/show_bug.cgi?id=1827551'); test.fixme(browserName === 'firefox' && isLinux, 'https://bugzilla.mozilla.org/show_bug.cgi?id=1827551');
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<form> <form>
<input type="file" multiple> <input type="file" multiple>
@ -166,9 +166,9 @@ await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-uplo
await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-upload.txt\", \"file-to-upload-2.txt\" });`); await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-upload.txt\", \"file-to-upload-2.txt\" });`);
}); });
test('should clear files', async ({ page, openRecorder, browserName, asset, isLinux }) => { test('should clear files', async ({ openRecorder, browserName, asset, isLinux }) => {
test.fixme(browserName === 'firefox' && isLinux, 'https://bugzilla.mozilla.org/show_bug.cgi?id=1827551'); test.fixme(browserName === 'firefox' && isLinux, 'https://bugzilla.mozilla.org/show_bug.cgi?id=1827551');
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<form> <form>
<input type="file" multiple> <input type="file" multiple>
@ -197,8 +197,8 @@ await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-uplo
await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { });`); await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { });`);
}); });
test('should download files', async ({ page, openRecorder, server }) => { test('should download files', async ({ openRecorder, server }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
server.setRoute('/download', (req, res) => { server.setRoute('/download', (req, res) => {
const pathName = url.parse(req.url!).path; const pathName = url.parse(req.url!).path;
@ -273,8 +273,8 @@ var download1 = await page.RunAndWaitForDownloadAsync(async () =>
});`); });`);
}); });
test('should handle dialogs', async ({ page, openRecorder }) => { test('should handle dialogs', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<button onclick="alert()">click me</button> <button onclick="alert()">click me</button>
@ -321,8 +321,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "click me" }).ClickAsync();
}); });
test('should handle history.postData', async ({ page, openRecorder, server }) => { test('should handle history.postData', async ({ openRecorder, server }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<script> <script>
@ -337,8 +337,9 @@ await page.GetByRole(AriaRole.Button, new() { Name = "click me" }).ClickAsync();
} }
}); });
test('should record open in a new tab with url', async ({ page, openRecorder, browserName }) => { test('should record open in a new tab with url', async ({ openRecorder, browserName, codegenMode }) => {
const recorder = await openRecorder(); test.skip(codegenMode === 'trace-events');
const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<a href="about:blank?foo">link</a>`); await recorder.setContentAndWait(`<a href="about:blank?foo">link</a>`);
const locator = await recorder.hoverOverElement('a'); const locator = await recorder.hoverOverElement('a');
@ -367,8 +368,8 @@ await page1.GotoAsync("about:blank?foo");`);
} }
}); });
test('should not clash pages', async ({ page, openRecorder, browserName }) => { test('should not clash pages', async ({ openRecorder, browserName }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
const [popup1] = await Promise.all([ const [popup1] = await Promise.all([
page.context().waitForEvent('page'), page.context().waitForEvent('page'),
page.evaluate(`window.open('about:blank')`) page.evaluate(`window.open('about:blank')`)
@ -404,8 +405,8 @@ await page1.GotoAsync("about:blank?foo");`);
expect(sources.get('C#')!.text).toContain(`await page2.Locator("#name").FillAsync("TextB");`); expect(sources.get('C#')!.text).toContain(`await page2.Locator("#name").FillAsync("TextB");`);
}); });
test('click should emit events in order', async ({ page, openRecorder }) => { test('click should emit events in order', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<button id=button> <button id=button>
@ -428,8 +429,8 @@ await page1.GotoAsync("about:blank?foo");`);
expect(messages).toEqual(['mousedown', 'mouseup', 'click']); expect(messages).toEqual(['mousedown', 'mouseup', 'click']);
}); });
test('should update hover model on action', async ({ page, openRecorder }) => { test('should update hover model on action', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name='updated'"></input>`); await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name='updated'"></input>`);
const [models] = await Promise.all([ const [models] = await Promise.all([
@ -439,8 +440,8 @@ await page1.GotoAsync("about:blank?foo");`);
expect(models.hovered).toBe('#checkbox'); expect(models.hovered).toBe('#checkbox');
}); });
test('should reset hover model on action when element detaches', async ({ page, openRecorder }) => { test('should reset hover model on action when element detaches', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input id="checkbox" onclick="document.getElementById('checkbox').remove()">`); await recorder.setContentAndWait(`<input id="checkbox" onclick="document.getElementById('checkbox').remove()">`);
const [models] = await Promise.all([ const [models] = await Promise.all([
@ -450,10 +451,10 @@ await page1.GotoAsync("about:blank?foo");`);
expect(models.hovered).toBe(null); expect(models.hovered).toBe(null);
}); });
test('should update active model on action', async ({ page, openRecorder, browserName, headless }) => { test('should update active model on action', async ({ openRecorder, browserName, headless }) => {
test.fixme(browserName === 'webkit'); test.fixme(browserName === 'webkit');
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name='updated'"></input>`); await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name='updated'"></input>`);
const [models] = await Promise.all([ const [models] = await Promise.all([
recorder.waitForActionPerformed(), recorder.waitForActionPerformed(),
@ -462,8 +463,8 @@ await page1.GotoAsync("about:blank?foo");`);
expect(models.active).toBe('#checkbox'); expect(models.active).toBe('#checkbox');
}); });
test('should check input with chaining id', async ({ page, openRecorder }) => { test('should check input with chaining id', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name = 'updated'"></input>`); await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name = 'updated'"></input>`);
await Promise.all([ await Promise.all([
recorder.waitForActionPerformed(), recorder.waitForActionPerformed(),
@ -471,8 +472,8 @@ await page1.GotoAsync("about:blank?foo");`);
]); ]);
}); });
test('should record navigations after identical pushState', async ({ page, openRecorder, server }) => { test('should record navigations after identical pushState', async ({ openRecorder, server }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
server.setRoute('/page2.html', (req, res) => { server.setRoute('/page2.html', (req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end('Hello world'); res.end('Hello world');
@ -490,7 +491,8 @@ await page1.GotoAsync("about:blank?foo");`);
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`); await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`);
}); });
test('should --save-trace', async ({ runCLI }, testInfo) => { test('should --save-trace', async ({ runCLI, codegenMode }, testInfo) => {
test.skip(codegenMode === 'trace-events');
const traceFileName = testInfo.outputPath('trace.zip'); const traceFileName = testInfo.outputPath('trace.zip');
const cli = runCLI([`--save-trace=${traceFileName}`], { const cli = runCLI([`--save-trace=${traceFileName}`], {
autoExitWhen: ' ', autoExitWhen: ' ',
@ -499,7 +501,8 @@ await page1.GotoAsync("about:blank?foo");`);
expect(fs.existsSync(traceFileName)).toBeTruthy(); expect(fs.existsSync(traceFileName)).toBeTruthy();
}); });
test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => { test('should save assets via SIGINT', async ({ runCLI, platform, codegenMode }, testInfo) => {
test.skip(codegenMode === 'trace-events');
test.skip(platform === 'win32', 'SIGINT not supported on Windows'); test.skip(platform === 'win32', 'SIGINT not supported on Windows');
const traceFileName = testInfo.outputPath('trace.zip'); const traceFileName = testInfo.outputPath('trace.zip');
@ -515,8 +518,8 @@ await page1.GotoAsync("about:blank?foo");`);
expect(fs.existsSync(harFileName)).toBeTruthy(); expect(fs.existsSync(harFileName)).toBeTruthy();
}); });
test('should fill tricky characters', async ({ page, openRecorder }) => { test('should fill tricky characters', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<textarea spellcheck=false id="textarea" name="name" oninput="console.log(textarea.value)"></textarea>`); await recorder.setContentAndWait(`<textarea spellcheck=false id="textarea" name="name" oninput="console.log(textarea.value)"></textarea>`);
const locator = await recorder.focusElement('textarea'); const locator = await recorder.focusElement('textarea');
@ -548,8 +551,8 @@ await page.Locator("#textarea").FillAsync(\"Hello'\\"\`\\nWorld\");`);
}); });
test('should --test-id-attribute', async ({ page, openRecorder }) => { test('should --test-id-attribute', async ({ openRecorder }) => {
const recorder = await openRecorder({ testIdAttributeName: 'my-test-id' }); const { page, recorder } = await openRecorder({ testIdAttributeName: 'my-test-id' });
await recorder.setContentAndWait(`<div my-test-id="foo">Hello</div>`); await recorder.setContentAndWait(`<div my-test-id="foo">Hello</div>`);
await page.click('[my-test-id=foo]'); await page.click('[my-test-id=foo]');

View file

@ -19,8 +19,8 @@ import { test, expect } from './inspectorTest';
test.describe('cli codegen', () => { test.describe('cli codegen', () => {
test.skip(({ mode }) => mode !== 'default'); test.skip(({ mode }) => mode !== 'default');
test('should click locator.first', async ({ page, openRecorder }) => { test('should click locator.first', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<button onclick="console.log('click1')">Submit</button> <button onclick="console.log('click1')">Submit</button>
@ -54,8 +54,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).First.ClickAsyn
expect(message.text()).toBe('click1'); expect(message.text()).toBe('click1');
}); });
test('should click locator.nth', async ({ page, openRecorder }) => { test('should click locator.nth', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<button onclick="console.log('click1')">Submit</button> <button onclick="console.log('click1')">Submit</button>
@ -89,8 +89,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).Nth(1).ClickAsy
expect(message.text()).toBe('click2'); expect(message.text()).toBe('click2');
}); });
test('should generate frame locators', async ({ page, openRecorder, server }) => { test('should generate frame locators', async ({ openRecorder, server }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
/* /*
iframe iframe
div Hello1 div Hello1
@ -127,13 +127,13 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).Nth(1).ClickAsy
page.locator("#frame1").contentFrame().getByText("Hello1").click();`); page.locator("#frame1").contentFrame().getByText("Hello1").click();`);
expect.soft(sources.get('Python')!.text).toContain(` expect.soft(sources.get('Python')!.text).toContain(`
page.locator("#frame1").content_frame().get_by_text("Hello1").click()`); page.locator("#frame1").content_frame.get_by_text("Hello1").click()`);
expect.soft(sources.get('Python Async')!.text).toContain(` expect.soft(sources.get('Python Async')!.text).toContain(`
await page.locator("#frame1").content_frame().get_by_text("Hello1").click()`); await page.locator("#frame1").content_frame.get_by_text("Hello1").click()`);
expect.soft(sources.get('C#')!.text).toContain(` expect.soft(sources.get('C#')!.text).toContain(`
await page.Locator("#frame1").ContentFrame().GetByText("Hello1").ClickAsync();`); await page.Locator("#frame1").ContentFrame.GetByText("Hello1").ClickAsync();`);
[sources] = await Promise.all([ [sources] = await Promise.all([
@ -148,13 +148,13 @@ await page.Locator("#frame1").ContentFrame().GetByText("Hello1").ClickAsync();`)
page.locator("#frame1").contentFrame().locator("iframe").contentFrame().getByText("Hello2").click();`); page.locator("#frame1").contentFrame().locator("iframe").contentFrame().getByText("Hello2").click();`);
expect.soft(sources.get('Python')!.text).toContain(` expect.soft(sources.get('Python')!.text).toContain(`
page.locator("#frame1").content_frame().locator("iframe").content_frame().get_by_text("Hello2").click()`); page.locator("#frame1").content_frame.locator("iframe").content_frame.get_by_text("Hello2").click()`);
expect.soft(sources.get('Python Async')!.text).toContain(` expect.soft(sources.get('Python Async')!.text).toContain(`
await page.locator("#frame1").content_frame().locator("iframe").content_frame().get_by_text("Hello2").click()`); await page.locator("#frame1").content_frame.locator("iframe").content_frame.get_by_text("Hello2").click()`);
expect.soft(sources.get('C#')!.text).toContain(` expect.soft(sources.get('C#')!.text).toContain(`
await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().GetByText("Hello2").ClickAsync();`); await page.Locator("#frame1").ContentFrame.Locator("iframe").ContentFrame.GetByText("Hello2").ClickAsync();`);
[sources] = await Promise.all([ [sources] = await Promise.all([
@ -169,13 +169,13 @@ await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().Ge
page.locator("#frame1").contentFrame().locator("iframe").contentFrame().locator("iframe[name=\\"one\\"]").contentFrame().getByText("HelloNameOne").click();`); page.locator("#frame1").contentFrame().locator("iframe").contentFrame().locator("iframe[name=\\"one\\"]").contentFrame().getByText("HelloNameOne").click();`);
expect.soft(sources.get('Python')!.text).toContain(` expect.soft(sources.get('Python')!.text).toContain(`
page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe[name=\\"one\\"]").content_frame().get_by_text("HelloNameOne").click()`); page.locator("#frame1").content_frame.locator("iframe").content_frame.locator("iframe[name=\\"one\\"]").content_frame.get_by_text("HelloNameOne").click()`);
expect.soft(sources.get('Python Async')!.text).toContain(` expect.soft(sources.get('Python Async')!.text).toContain(`
await page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe[name=\\"one\\"]").content_frame().get_by_text("HelloNameOne").click()`); await page.locator("#frame1").content_frame.locator("iframe").content_frame.locator("iframe[name=\\"one\\"]").content_frame.get_by_text("HelloNameOne").click()`);
expect.soft(sources.get('C#')!.text).toContain(` expect.soft(sources.get('C#')!.text).toContain(`
await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().Locator("iframe[name=\\"one\\"]").ContentFrame().GetByText("HelloNameOne").ClickAsync();`); await page.Locator("#frame1").ContentFrame.Locator("iframe").ContentFrame.Locator("iframe[name=\\"one\\"]").ContentFrame.GetByText("HelloNameOne").ClickAsync();`);
[sources] = await Promise.all([ [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'HelloNameAnonymous'), recorder.waitForOutput('JavaScript', 'HelloNameAnonymous'),
@ -189,17 +189,17 @@ await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().Lo
page.locator("#frame1").contentFrame().locator("iframe").contentFrame().locator("iframe").nth(2).contentFrame().getByText("HelloNameAnonymous").click();`); page.locator("#frame1").contentFrame().locator("iframe").contentFrame().locator("iframe").nth(2).contentFrame().getByText("HelloNameAnonymous").click();`);
expect.soft(sources.get('Python')!.text).toContain(` expect.soft(sources.get('Python')!.text).toContain(`
page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe").nth(2).content_frame().get_by_text("HelloNameAnonymous").click()`); page.locator("#frame1").content_frame.locator("iframe").content_frame.locator("iframe").nth(2).content_frame.get_by_text("HelloNameAnonymous").click()`);
expect.soft(sources.get('Python Async')!.text).toContain(` expect.soft(sources.get('Python Async')!.text).toContain(`
await page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe").nth(2).content_frame().get_by_text("HelloNameAnonymous").click()`); await page.locator("#frame1").content_frame.locator("iframe").content_frame.locator("iframe").nth(2).content_frame.get_by_text("HelloNameAnonymous").click()`);
expect.soft(sources.get('C#')!.text).toContain(` expect.soft(sources.get('C#')!.text).toContain(`
await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().Locator("iframe").Nth(2).ContentFrame().GetByText("HelloNameAnonymous").ClickAsync();`); await page.Locator("#frame1").ContentFrame.Locator("iframe").ContentFrame.Locator("iframe").Nth(2).ContentFrame.GetByText("HelloNameAnonymous").ClickAsync();`);
}); });
test('should generate frame locators with special characters in name attribute', async ({ page, openRecorder, server }) => { test('should generate frame locators with special characters in name attribute', async ({ openRecorder, server }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<iframe srcdoc="<button>Click me</button>"> <iframe srcdoc="<button>Click me</button>">
`, server.EMPTY_PAGE, 2); `, server.EMPTY_PAGE, 2);
@ -217,17 +217,17 @@ await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().Lo
page.locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Click me")).click()`); page.locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Click me")).click()`);
expect.soft(sources.get('Python')!.text).toContain(` expect.soft(sources.get('Python')!.text).toContain(`
page.locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").content_frame().get_by_role("button", name="Click me").click()`); page.locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").content_frame.get_by_role("button", name="Click me").click()`);
expect.soft(sources.get('Python Async')!.text).toContain(` expect.soft(sources.get('Python Async')!.text).toContain(`
await page.locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").content_frame().get_by_role("button", name="Click me").click()`); await page.locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").content_frame.get_by_role("button", name="Click me").click()`);
expect.soft(sources.get('C#')!.text).toContain(` expect.soft(sources.get('C#')!.text).toContain(`
await page.Locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`); await page.Locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").ContentFrame.GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`);
}); });
test('should generate frame locators with title attribute', async ({ page, openRecorder, server }) => { test('should generate frame locators with title attribute', async ({ openRecorder, server }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<iframe title="hello world" srcdoc="<button>Click me</button>"></iframe> <iframe title="hello world" srcdoc="<button>Click me</button>"></iframe>
`, server.EMPTY_PAGE, 1); `, server.EMPTY_PAGE, 1);
@ -246,20 +246,20 @@ await page.Locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").Cont
); );
expect.soft(sources.get('Python')!.text).toContain( expect.soft(sources.get('Python')!.text).toContain(
`page.locator(\"iframe[title=\\\"hello world\\\"]\").content_frame().get_by_role(\"button\", name=\"Click me\").click()` `page.locator(\"iframe[title=\\\"hello world\\\"]\").content_frame.get_by_role(\"button\", name=\"Click me\").click()`
); );
expect.soft(sources.get('Python Async')!.text).toContain( expect.soft(sources.get('Python Async')!.text).toContain(
`await page.locator("iframe[title=\\\"hello world\\\"]").content_frame().get_by_role("button", name="Click me").click()` `await page.locator("iframe[title=\\\"hello world\\\"]").content_frame.get_by_role("button", name="Click me").click()`
); );
expect.soft(sources.get('C#')!.text).toContain( expect.soft(sources.get('C#')!.text).toContain(
`await page.Locator("iframe[title=\\\"hello world\\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` `await page.Locator("iframe[title=\\\"hello world\\\"]").ContentFrame.GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
); );
}); });
test('should generate frame locators with name attribute', async ({ page, openRecorder, server }) => { test('should generate frame locators with name attribute', async ({ openRecorder, server }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<iframe name="hello world" srcdoc="<button>Click me</button>"></iframe> <iframe name="hello world" srcdoc="<button>Click me</button>"></iframe>
`, server.EMPTY_PAGE, 1); `, server.EMPTY_PAGE, 1);
@ -278,20 +278,20 @@ await page.Locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").Cont
); );
expect.soft(sources.get('Python')!.text).toContain( expect.soft(sources.get('Python')!.text).toContain(
`page.locator(\"iframe[name=\\\"hello world\\\"]\").content_frame().get_by_role(\"button\", name=\"Click me\").click()` `page.locator(\"iframe[name=\\\"hello world\\\"]\").content_frame.get_by_role(\"button\", name=\"Click me\").click()`
); );
expect.soft(sources.get('Python Async')!.text).toContain( expect.soft(sources.get('Python Async')!.text).toContain(
`await page.locator("iframe[name=\\\"hello world\\\"]").content_frame().get_by_role("button", name="Click me").click()` `await page.locator("iframe[name=\\\"hello world\\\"]").content_frame.get_by_role("button", name="Click me").click()`
); );
expect.soft(sources.get('C#')!.text).toContain( expect.soft(sources.get('C#')!.text).toContain(
`await page.Locator("iframe[name=\\\"hello world\\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` `await page.Locator("iframe[name=\\\"hello world\\\"]").ContentFrame.GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
); );
}); });
test('should generate frame locators with id attribute', async ({ page, openRecorder, server }) => { test('should generate frame locators with id attribute', async ({ openRecorder, server }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<iframe id="hello-world" srcdoc="<button>Click me</button>"></iframe> <iframe id="hello-world" srcdoc="<button>Click me</button>"></iframe>
`, server.EMPTY_PAGE, 1); `, server.EMPTY_PAGE, 1);
@ -310,20 +310,20 @@ await page.Locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").Cont
); );
expect.soft(sources.get('Python')!.text).toContain( expect.soft(sources.get('Python')!.text).toContain(
`page.locator(\"#hello-world\").content_frame().get_by_role(\"button\", name=\"Click me\").click()` `page.locator(\"#hello-world\").content_frame.get_by_role(\"button\", name=\"Click me\").click()`
); );
expect.soft(sources.get('Python Async')!.text).toContain( expect.soft(sources.get('Python Async')!.text).toContain(
`await page.locator("#hello-world").content_frame().get_by_role("button", name="Click me").click()` `await page.locator("#hello-world").content_frame.get_by_role("button", name="Click me").click()`
); );
expect.soft(sources.get('C#')!.text).toContain( expect.soft(sources.get('C#')!.text).toContain(
`await page.Locator("#hello-world").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` `await page.Locator("#hello-world").ContentFrame.GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
); );
}); });
test('should generate frame locators with testId', async ({ page, openRecorder, server }) => { test('should generate frame locators with testId', async ({ openRecorder, server }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<iframe data-testid="my-testid" srcdoc="<button>Click me</button>"></iframe> <iframe data-testid="my-testid" srcdoc="<button>Click me</button>"></iframe>
`, server.EMPTY_PAGE, 1); `, server.EMPTY_PAGE, 1);
@ -342,20 +342,20 @@ await page.Locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").Cont
); );
expect.soft(sources.get('Python')!.text).toContain( expect.soft(sources.get('Python')!.text).toContain(
`page.locator(\"[data-testid=\\\"my-testid\\\"]\").content_frame().get_by_role(\"button\", name=\"Click me\").click()` `page.locator(\"[data-testid=\\\"my-testid\\\"]\").content_frame.get_by_role(\"button\", name=\"Click me\").click()`
); );
expect.soft(sources.get('Python Async')!.text).toContain( expect.soft(sources.get('Python Async')!.text).toContain(
`await page.locator("[data-testid=\\\"my-testid\\\"]").content_frame().get_by_role("button", name="Click me").click()` `await page.locator("[data-testid=\\\"my-testid\\\"]").content_frame.get_by_role("button", name="Click me").click()`
); );
expect.soft(sources.get('C#')!.text).toContain( expect.soft(sources.get('C#')!.text).toContain(
`await page.Locator("[data-testid=\\\"my-testid\\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` `await page.Locator("[data-testid=\\\"my-testid\\\"]").ContentFrame.GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
); );
}); });
test('should generate role locators undef frame locators', async ({ page, openRecorder, server }) => { test('should generate role locators undef frame locators', async ({ openRecorder, server }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<iframe id=frame1 srcdoc="<button>Submit</button>">`, server.EMPTY_PAGE, 2); await recorder.setContentAndWait(`<iframe id=frame1 srcdoc="<button>Submit</button>">`, server.EMPTY_PAGE, 2);
const frame = page.mainFrame().childFrames()[0]; const frame = page.mainFrame().childFrames()[0];
@ -371,17 +371,17 @@ await page.Locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").Cont
page.locator("#frame1").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Submit")).click();`); page.locator("#frame1").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Submit")).click();`);
expect.soft(sources.get('Python')!.text).toContain(` expect.soft(sources.get('Python')!.text).toContain(`
page.locator("#frame1").content_frame().get_by_role("button", name="Submit").click()`); page.locator("#frame1").content_frame.get_by_role("button", name="Submit").click()`);
expect.soft(sources.get('Python Async')!.text).toContain(` expect.soft(sources.get('Python Async')!.text).toContain(`
await page.locator("#frame1").content_frame().get_by_role("button", name="Submit").click()`); await page.locator("#frame1").content_frame.get_by_role("button", name="Submit").click()`);
expect.soft(sources.get('C#')!.text).toContain(` expect.soft(sources.get('C#')!.text).toContain(`
await page.Locator("#frame1").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`); await page.Locator("#frame1").ContentFrame.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`);
}); });
test('should generate getByTestId', async ({ page, openRecorder }) => { test('should generate getByTestId', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(`<div data-testid=testid onclick="console.log('click')">Submit</div>`); await recorder.setContentAndWait(`<div data-testid=testid onclick="console.log('click')">Submit</div>`);
@ -412,8 +412,8 @@ await page.GetByTestId("testid").ClickAsync();`);
expect(message.text()).toBe('click'); expect(message.text()).toBe('click');
}); });
test('should generate getByPlaceholder', async ({ page, openRecorder }) => { test('should generate getByPlaceholder', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<input placeholder="Country"></input>`); await recorder.setContentAndWait(`<input placeholder="Country"></input>`);
@ -441,8 +441,8 @@ await page.GetByTestId("testid").ClickAsync();`);
await page.GetByPlaceholder("Country").ClickAsync();`); await page.GetByPlaceholder("Country").ClickAsync();`);
}); });
test('should generate getByAltText', async ({ page, openRecorder }) => { test('should generate getByAltText', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<input alt="Country"></input>`); await recorder.setContentAndWait(`<input alt="Country"></input>`);
@ -470,8 +470,8 @@ await page.GetByPlaceholder("Country").ClickAsync();`);
await page.GetByAltText("Country").ClickAsync();`); await page.GetByAltText("Country").ClickAsync();`);
}); });
test('should generate getByLabel', async ({ page, openRecorder }) => { test('should generate getByLabel', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<label for=target>Country</label><input id=target>`); await recorder.setContentAndWait(`<label for=target>Country</label><input id=target>`);
@ -499,8 +499,8 @@ await page.GetByAltText("Country").ClickAsync();`);
await page.GetByLabel("Country").ClickAsync();`); await page.GetByLabel("Country").ClickAsync();`);
}); });
test('should generate getByLabel without regex', async ({ page, openRecorder }) => { test('should generate getByLabel without regex', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<label for=target>Coun"try</label><input id=target>`); await recorder.setContentAndWait(`<label for=target>Coun"try</label><input id=target>`);
@ -528,8 +528,8 @@ await page.GetByLabel("Country").ClickAsync();`);
await page.GetByLabel("Coun\\"try").ClickAsync();`); await page.GetByLabel("Coun\\"try").ClickAsync();`);
}); });
test('should consume pointer events', async ({ page, openRecorder }) => { test('should consume pointer events', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<button onclick="console.log('clicked')">Submit</button> <button onclick="console.log('clicked')">Submit</button>
@ -559,8 +559,8 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
]); ]);
}); });
test('should consume contextmenu events, despite a custom context menu', async ({ page, openRecorder, browserName, platform }) => { test('should consume contextmenu events, despite a custom context menu', async ({ openRecorder, browserName, platform }) => {
const recorder = await openRecorder(); const { page, recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<button>Right click me.</button> <button>Right click me.</button>
@ -630,7 +630,7 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
}); });
test('should assert value', async ({ openRecorder }) => { test('should assert value', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<input id=first value=foo> <input id=first value=foo>
@ -679,7 +679,7 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
test('should assert value on disabled input', async ({ openRecorder, browserName }) => { test('should assert value on disabled input', async ({ openRecorder, browserName }) => {
test.fixme(browserName === 'firefox', 'pointerup event is not dispatched on a disabled input'); test.fixme(browserName === 'firefox', 'pointerup event is not dispatched on a disabled input');
const recorder = await openRecorder(); const { recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<input id=first value=foo> <input id=first value=foo>
@ -702,7 +702,7 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
}); });
test('should assert value on disabled select', async ({ openRecorder, browserName }) => { test('should assert value on disabled select', async ({ openRecorder, browserName }) => {
const recorder = await openRecorder(); const { recorder } = await openRecorder();
await recorder.setContentAndWait(` await recorder.setContentAndWait(`
<select id=first><option value=foo1>Foo1</option><option value=bar1>Bar1</option></select> <select id=first><option value=foo1>Foo1</option><option value=bar1>Bar1</option></select>
@ -723,7 +723,7 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
}); });
test('should assert visibility', async ({ openRecorder }) => { test('should assert visibility', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<input>`); await recorder.setContentAndWait(`<input>`);
@ -741,10 +741,10 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
}); });
test('should keep toolbar visible even if webpage erases content in hydration', async ({ openRecorder }) => { test('should keep toolbar visible even if webpage erases content in hydration', async ({ openRecorder }) => {
const recorder = await openRecorder(); const { recorder } = await openRecorder();
const hydrate = () => { const hydrate = () => {
setTimeout(() => { window.builtinSetTimeout(() => {
document.documentElement.innerHTML = '<p>Post-Hydration Content</p>'; document.documentElement.innerHTML = '<p>Post-Hydration Content</p>';
}, 500); }, 500);
}; };

View file

@ -27,7 +27,7 @@ export { expect } from '@playwright/test';
type CLITestArgs = { type CLITestArgs = {
recorderPageGetter: () => Promise<Page>; recorderPageGetter: () => Promise<Page>;
closeRecorder: () => Promise<void>; closeRecorder: () => Promise<void>;
openRecorder: (options?: { testIdAttributeName: string }) => Promise<Recorder>; openRecorder: (options?: { testIdAttributeName: string }) => Promise<{ recorder: Recorder, page: Page }>;
runCLI: (args: string[], options?: { autoExitWhen?: string }) => CLIMock; runCLI: (args: string[], options?: { autoExitWhen?: string }) => CLIMock;
}; };
@ -50,12 +50,11 @@ const playwrightToAutomateInspector = require('../../../packages/playwright-core
export const test = contextTest.extend<CLITestArgs>({ export const test = contextTest.extend<CLITestArgs>({
recorderPageGetter: async ({ context, toImpl, mode }, run, testInfo) => { recorderPageGetter: async ({ context, toImpl, mode }, run, testInfo) => {
process.env.PWTEST_RECORDER_PORT = String(10907 + testInfo.workerIndex);
testInfo.skip(mode.startsWith('service')); testInfo.skip(mode.startsWith('service'));
await run(async () => { await run(async () => {
while (!toImpl(context).recorderAppForTest) while (!toImpl(context).recorderAppForTest)
await new Promise(f => setTimeout(f, 100)); await new Promise(f => setTimeout(f, 100));
const wsEndpoint = toImpl(context).recorderAppForTest.wsEndpoint; const wsEndpoint = toImpl(context).recorderAppForTest.wsEndpointForTest;
const browser = await playwrightToAutomateInspector.chromium.connectOverCDP({ wsEndpoint }); const browser = await playwrightToAutomateInspector.chromium.connectOverCDP({ wsEndpoint });
const c = browser.contexts()[0]; const c = browser.contexts()[0];
return c.pages()[0] || await c.waitForEvent('page'); return c.pages()[0] || await c.waitForEvent('page');
@ -68,19 +67,32 @@ export const test = contextTest.extend<CLITestArgs>({
}); });
}, },
runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions }, run, testInfo) => { runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions, codegenMode }, run, testInfo) => {
process.env.PWTEST_RECORDER_PORT = String(10907 + testInfo.workerIndex);
testInfo.skip(mode.startsWith('service')); testInfo.skip(mode.startsWith('service'));
await run((cliArgs, { autoExitWhen } = {}) => { await run((cliArgs, { autoExitWhen } = {}) => {
return new CLIMock(childProcess, browserName, channel, headless, cliArgs, launchOptions.executablePath, autoExitWhen); return new CLIMock(childProcess, {
browserName,
channel,
headless,
args: cliArgs,
executablePath: launchOptions.executablePath,
autoExitWhen,
codegenMode
});
}); });
}, },
openRecorder: async ({ page, recorderPageGetter }, run) => { openRecorder: async ({ context, recorderPageGetter, codegenMode }, run) => {
await run(async (options?: { testIdAttributeName?: string }) => { await run(async (options?: { testIdAttributeName?: string }) => {
await (page.context() as any)._enableRecorder({ language: 'javascript', mode: 'recording', ...options }); await (context as any)._enableRecorder({
return new Recorder(page, await recorderPageGetter()); language: 'javascript',
mode: 'recording',
codegenMode,
...options
});
const page = await context.newPage();
return { page, recorder: new Recorder(page, await recorderPageGetter()) };
}); });
}, },
}); });
@ -206,23 +218,24 @@ class Recorder {
class CLIMock { class CLIMock {
process: TestChildProcess; process: TestChildProcess;
constructor(childProcess: CommonFixtures['childProcess'], browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined) { constructor(childProcess: CommonFixtures['childProcess'], options: { browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined, codegenMode?: 'trace-events' | 'actions'}) {
const nodeArgs = [ const nodeArgs = [
'node', 'node',
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'), path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'),
'codegen', 'codegen',
...args, ...options.args,
`--browser=${browserName}`, `--browser=${options.browserName}`,
]; ];
if (channel) if (options.channel)
nodeArgs.push(`--channel=${channel}`); nodeArgs.push(`--channel=${options.channel}`);
this.process = childProcess({ this.process = childProcess({
command: nodeArgs, command: nodeArgs,
env: { env: {
PWTEST_CLI_AUTO_EXIT_WHEN: autoExitWhen, PW_RECORDER_IS_TRACE_VIEWER: options.codegenMode === 'trace-events' ? '1' : undefined,
PWTEST_CLI_AUTO_EXIT_WHEN: options.autoExitWhen,
PWTEST_CLI_IS_UNDER_TEST: '1', PWTEST_CLI_IS_UNDER_TEST: '1',
PWTEST_CLI_HEADLESS: headless ? '1' : undefined, PWTEST_CLI_HEADLESS: options.headless ? '1' : undefined,
PWTEST_CLI_EXECUTABLE_PATH: executablePath, PWTEST_CLI_EXECUTABLE_PATH: options.executablePath,
DEBUG: (process.env.DEBUG ?? '') + ',pw:browser*', DEBUG: (process.env.DEBUG ?? '') + ',pw:browser*',
}, },
}); });

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