Compare commits

...

29 commits

Author SHA1 Message Date
Max Schmitt ad14680e1d
cherry-pick(1.48): remove old CDNs (#34101) 2024-12-27 13:42:58 +01:00
Max Schmitt dc80964a3f
chore: mark v1.48.2 (#33290) 2024-10-25 22:23:05 +02:00
Max Schmitt ffd19e580e cherry-pick(#33269): fix(codegen): SIGINT handling was leading to zombie processes 2024-10-24 23:05:10 +02:00
Dmitry Gozman f26c6fc226
cherry-pick(#33240, #33264): fix(recorder): do not leak when instantiated in snapshots (#33259) 2024-10-24 04:14:04 -07:00
Dmitry Gozman ff1932b68c
cherry-pick(#33244): fix(trace viewer): limit the number of contexts loaded in sw (#33261) 2024-10-24 02:23:56 -07:00
Dmitry Gozman a96f4832e1
cherry-pick(#33245): fix(trace viewer): make LRUCache per-trace (#33260) 2024-10-24 02:23:44 -07:00
Max Schmitt 8e96d946aa cherry-pick(#33211): docs: use WebSocketFrame abstraction for Java & .NET 2024-10-21 21:22:00 +02:00
Debbie O'Brien 5b540676f2 cherry-pick(#33147): docs: add video to release notes 2024-10-16 18:38:30 +02:00
Max Schmitt ceb756dad3
chore: mark v1.48.1 (#33136) 2024-10-16 11:31:18 +02:00
Lars Hanisch c3740d37af cherry-pick(#33133): (docker): correct Ubuntu Noble name in name template 2024-10-16 10:16:42 +02:00
Pavel Feldman 2ec0c86b93 cherry-pick(#33124): test: unflake ff debugger test 2024-10-15 16:23:58 -07:00
Pavel Feldman 8ef381fc5f cherry-pick(#33122): chore: fix ff test for codegen 2024-10-15 13:35:28 -07:00
Dmitry Gozman c72a2538bc
cherry-pick(#33110): fix(chromium): disable PlzDedicatedWorker again (#33113) 2024-10-15 04:56:49 -07:00
Dmitry Gozman 3d7ef3c062
cherry-pick(#33095): fix(routeWebSocket): make sure ws url without trailing slash is supported (#33112) 2024-10-15 04:56:29 -07:00
Dmitry Gozman 78c43bc5d3 cherry-pick(#33097): docs: improve docs for WebSocketRoute 2024-10-15 10:07:44 +01:00
Pavel Feldman 6dc9ec7fe9 cherry-pick(#33099): chore: fix codegen selector while debugging 2024-10-14 14:05:42 -07:00
Max Schmitt e5bbd5effe cherry-pick(#33096): chore: various v1.48.0 roll fixes for .NET 2024-10-14 16:33:00 +02:00
Pavel Feldman daff1a9025 cherry-pick(#33030): fix(ui): bring back the headed param 2024-10-09 17:47:50 -07:00
Pavel Feldman 8d524e24ce cherry-pick(#32996): chore: allow minimal height for trace attachments 2024-10-08 10:31:10 -07:00
Max Schmitt 0cdbb11068
chore: mark v1.48.0 (#33009) 2024-10-08 14:37:38 +02:00
Dmitry Gozman ca368d43fa
cherry-pick(#32991): fix(routeWebSocket): do not show in the trace (#33004) 2024-10-08 03:56:00 -07:00
Max Schmitt c329c5c1ec cherry-pick(#33005): chore(driver): roll driver to recent Node.js LTS version 2024-10-08 12:54:50 +02:00
Simon Knott 97aaa12be4
cherry-pick(#32956): fix(fetch): listener leaks on Socket
Closes https://github.com/microsoft/playwright/issues/32951

`node:http` reuses TCP Sockets under the hood. We weren't cleaning up
our listeners, leading to the `MaxListenersExceededWarning`.

This PR adds cleanup logic. It also raises the warning threshhold, so
that it doesn't trigger until there's 100 concurrent requests over the
same socket.
2024-10-08 09:18:24 +02:00
Max Schmitt 0c17732e91 cherry-pick(#32949): feat(chromium): roll to r1140 2024-10-04 11:34:50 +02:00
Playwright Service 530f04304e
cherry-pick(#32938): feat(firefox): roll to r1465
This PR cherry-picks the following commits:
- 616425a0fb
2024-10-03 09:15:18 +02:00
Max Schmitt 1054930edd cherry-pick(#32924): docs: fix Java/.NET types for docs rolling 2024-10-02 13:30:41 +02:00
Playwright Service e732f68eee cherry-pick(#32906): feat(chromium): roll to r1139
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-10-01 19:06:53 +02:00
Max Schmitt dfa0e8bf35 cherry-pick(#32905): chore: remove 'screenshot instead of snapshot' usages 2024-10-01 18:38:59 +02:00
Max Schmitt 7155356e3c cherry-pick(#32880): chore: unflake 'should record' 2024-09-30 20:33:37 +02:00
75 changed files with 705 additions and 314 deletions

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

View file

@ -3699,8 +3699,8 @@ await page.routeWebSocket('/ws', ws => {
```java ```java
page.routeWebSocket("/ws", ws -> { page.routeWebSocket("/ws", ws -> {
ws.onMessage(message -> { ws.onMessage(frame -> {
if ("request".equals(message)) if ("request".equals(frame.text()))
ws.send("response"); ws.send("response");
}); });
}); });
@ -3730,8 +3730,8 @@ page.route_web_socket("/ws", handler)
```csharp ```csharp
await page.RouteWebSocketAsync("/ws", ws => { await page.RouteWebSocketAsync("/ws", ws => {
ws.OnMessage(message => { ws.OnMessage(frame => {
if (message == "request") if (frame.Text == "request")
ws.Send("response"); ws.Send("response");
}); });
}); });

View file

@ -8,7 +8,7 @@ Whenever a [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSoc
By default, the routed WebSocket will not connect to the server. This way, you can mock entire communcation over the WebSocket. Here is an example that responds to a `"request"` with a `"response"`. By default, the routed WebSocket will not connect to the server. This way, you can mock entire communcation over the WebSocket. Here is an example that responds to a `"request"` with a `"response"`.
```js ```js
await page.routeWebSocket('/ws', ws => { await page.routeWebSocket('wss://example.com/ws', ws => {
ws.onMessage(message => { ws.onMessage(message => {
if (message === 'request') if (message === 'request')
ws.send('response'); ws.send('response');
@ -17,9 +17,9 @@ await page.routeWebSocket('/ws', ws => {
``` ```
```java ```java
page.routeWebSocket("/ws", ws -> { page.routeWebSocket("wss://example.com/ws", ws -> {
ws.onMessage(message -> { ws.onMessage(frame -> {
if ("request".equals(message)) if ("request".equals(frame.text()))
ws.send("response"); ws.send("response");
}); });
}); });
@ -30,7 +30,7 @@ def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
if message == "request": if message == "request":
ws.send("response") ws.send("response")
await page.route_web_socket("/ws", lambda ws: ws.on_message( await page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
lambda message: message_handler(ws, message) lambda message: message_handler(ws, message)
)) ))
``` ```
@ -40,15 +40,15 @@ def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
if message == "request": if message == "request":
ws.send("response") ws.send("response")
page.route_web_socket("/ws", lambda ws: ws.on_message( page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
lambda message: message_handler(ws, message) lambda message: message_handler(ws, message)
)) ))
``` ```
```csharp ```csharp
await page.RouteWebSocketAsync("/ws", ws => { await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
ws.OnMessage(message => { ws.OnMessage(frame => {
if (message == "request") if (frame.Text == "request")
ws.Send("response"); ws.Send("response");
}); });
}); });
@ -56,6 +56,69 @@ await page.RouteWebSocketAsync("/ws", ws => {
Since we do not call [`method: WebSocketRoute.connectToServer`] inside the WebSocket route handler, Playwright assumes that WebSocket will be mocked, and opens the WebSocket inside the page automatically. Since we do not call [`method: WebSocketRoute.connectToServer`] inside the WebSocket route handler, Playwright assumes that WebSocket will be mocked, and opens the WebSocket inside the page automatically.
Here is another example that handles JSON messages:
```js
await page.routeWebSocket('wss://example.com/ws', ws => {
ws.onMessage(message => {
const json = JSON.parse(message);
if (json.request === 'question')
ws.send(JSON.stringify({ response: 'answer' }));
});
});
```
```java
page.routeWebSocket("wss://example.com/ws", ws -> {
ws.onMessage(frame -> {
JsonObject json = new JsonParser().parse(frame.text()).getAsJsonObject();
if ("question".equals(json.get("request").getAsString())) {
Map<String, String> result = new HashMap();
result.put("response", "answer");
ws.send(gson.toJson(result));
}
});
});
```
```python async
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
json_message = json.loads(message)
if json_message["request"] == "question":
ws.send(json.dumps({ "response": "answer" }))
await page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
lambda message: message_handler(ws, message)
))
```
```python sync
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
json_message = json.loads(message)
if json_message["request"] == "question":
ws.send(json.dumps({ "response": "answer" }))
page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
lambda message: message_handler(ws, message)
))
```
```csharp
await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
ws.OnMessage(frame => {
using var jsonDoc = JsonDocument.Parse(frame.Text);
JsonElement root = jsonDoc.RootElement;
if (root.TryGetProperty("request", out JsonElement requestElement) && requestElement.GetString() == "question")
{
var response = new Dictionary<string, string> { ["response"] = "answer" };
string jsonResponse = JsonSerializer.Serialize(response);
ws.Send(jsonResponse);
}
});
});
```
**Intercepting** **Intercepting**
Alternatively, you may want to connect to the actual server, but intercept messages in-between and modify or block them. Calling [`method: WebSocketRoute.connectToServer`] returns a server-side `WebSocketRoute` instance that you can send messages to, or handle incoming messages. Alternatively, you may want to connect to the actual server, but intercept messages in-between and modify or block them. Calling [`method: WebSocketRoute.connectToServer`] returns a server-side `WebSocketRoute` instance that you can send messages to, or handle incoming messages.
@ -77,11 +140,11 @@ await page.routeWebSocket('/ws', ws => {
```java ```java
page.routeWebSocket("/ws", ws -> { page.routeWebSocket("/ws", ws -> {
WebSocketRoute server = ws.connectToServer(); WebSocketRoute server = ws.connectToServer();
ws.onMessage(message -> { ws.onMessage(frame -> {
if ("request".equals(message)) if ("request".equals(frame.text()))
server.send("request2"); server.send("request2");
else else
server.send(message); server.send(frame.text());
}); });
}); });
``` ```
@ -117,11 +180,11 @@ page.route_web_socket("/ws", handler)
```csharp ```csharp
await page.RouteWebSocketAsync("/ws", ws => { await page.RouteWebSocketAsync("/ws", ws => {
var server = ws.ConnectToServer(); var server = ws.ConnectToServer();
ws.OnMessage(message => { ws.OnMessage(frame => {
if (message == "request") if (frame.Text == "request")
server.Send("request2"); server.Send("request2");
else else
server.Send(message); server.Send(frame.Text);
}); });
}); });
``` ```
@ -152,13 +215,13 @@ await page.routeWebSocket('/ws', ws => {
```java ```java
page.routeWebSocket("/ws", ws -> { page.routeWebSocket("/ws", ws -> {
WebSocketRoute server = ws.connectToServer(); WebSocketRoute server = ws.connectToServer();
ws.onMessage(message -> { ws.onMessage(frame -> {
if (!"blocked-from-the-page".equals(message)) if (!"blocked-from-the-page".equals(frame.text()))
server.send(message); server.send(frame.text());
}); });
server.onMessage(message -> { server.onMessage(frame -> {
if (!"blocked-from-the-server".equals(message)) if (!"blocked-from-the-server".equals(frame.text()))
ws.send(message); ws.send(frame.text());
}); });
}); });
``` ```
@ -200,13 +263,13 @@ page.route_web_socket("/ws", handler)
```csharp ```csharp
await page.RouteWebSocketAsync("/ws", ws => { await page.RouteWebSocketAsync("/ws", ws => {
var server = ws.ConnectToServer(); var server = ws.ConnectToServer();
ws.OnMessage(message => { ws.OnMessage(frame => {
if (message != "blocked-from-the-page") if (frame.Text != "blocked-from-the-page")
server.Send(message); server.Send(frame.Text);
}); });
server.OnMessage(message => { server.OnMessage(frame => {
if (message != "blocked-from-the-server") if (frame.Text != "blocked-from-the-server")
ws.Send(message); ws.Send(frame.Text);
}); });
}); });
``` ```
@ -255,13 +318,26 @@ By default, closing one side of the connection, either in the page or on the ser
### param: WebSocketRoute.onClose.handler ### param: WebSocketRoute.onClose.handler
* since: v1.48 * since: v1.48
- `handler` <[function]\([number]|[undefined], [string]|[undefined]\): [Promise<any>|any]> * langs: js, python
- `handler` <[function]\([int]|[undefined], [string]|[undefined]\): [Promise<any>|any]>
Function that will handle WebSocket closure. Received an optional [close code](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#code) and an optional [close reason](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#reason). Function that will handle WebSocket closure. Received an optional [close code](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#code) and an optional [close reason](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#reason).
### param: WebSocketRoute.onClose.handler
* since: v1.48
* langs: java
- `handler` <[function]\([null]|[int], [null]|[string]\)>
Function that will handle WebSocket closure. Received an optional [close code](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#code) and an optional [close reason](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#reason).
## async method: WebSocketRoute.onMessage ### param: WebSocketRoute.onClose.handler
* since: v1.48
* langs: csharp
- `handler` <[function]\([int?], [string]\)>
Function that will handle WebSocket closure. Received an optional [close code](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#code) and an optional [close reason](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#reason).
## method: WebSocketRoute.onMessage
* since: v1.48 * since: v1.48
This method allows to handle messages that are sent by the WebSocket, either from the page or from the server. This method allows to handle messages that are sent by the WebSocket, either from the page or from the server.

View file

@ -363,7 +363,7 @@ Target URL.
## js-fetch-option-params ## js-fetch-option-params
* langs: js * langs: js
- `params` <[Object]<[string], [string]|[number]|[boolean]>|[URLSearchParams]|[string]> - `params` <[Object]<[string], [string]|[float]|[boolean]>|[URLSearchParams]|[string]>
Query parameters to be sent with the URL. Query parameters to be sent with the URL.

View file

@ -434,3 +434,122 @@ pwsh bin/Debug/netX/playwright.ps1 open --save-har=example.har --save-har-glob="
``` ```
Read more about [advanced networking](./network.md). Read more about [advanced networking](./network.md).
## Mock WebSockets
The following code will intercept WebSocket connections and mock entire communcation over the WebSocket, instead of connecting to the server. This example responds to a `"request"` with a `"response"`.
```js
await page.routeWebSocket('wss://example.com/ws', ws => {
ws.onMessage(message => {
if (message === 'request')
ws.send('response');
});
});
```
```java
page.routeWebSocket("wss://example.com/ws", ws -> {
ws.onMessage(frame -> {
if ("request".equals(frame.text()))
ws.send("response");
});
});
```
```python async
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
if message == "request":
ws.send("response")
await page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
lambda message: message_handler(ws, message)
))
```
```python sync
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
if message == "request":
ws.send("response")
page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
lambda message: message_handler(ws, message)
))
```
```csharp
await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
ws.OnMessage(frame => {
if (frame.Text == "request")
ws.Send("response");
});
});
```
Alternatively, you may want to connect to the actual server, but intercept messages in-between and modify or block them. Here is an example that modifies some of the messages sent by the page to the server, and leaves the rest unmodified.
```js
await page.routeWebSocket('wss://example.com/ws', ws => {
const server = ws.connectToServer();
ws.onMessage(message => {
if (message === 'request')
server.send('request2');
else
server.send(message);
});
});
```
```java
page.routeWebSocket("wss://example.com/ws", ws -> {
WebSocketRoute server = ws.connectToServer();
ws.onMessage(frame -> {
if ("request".equals(frame.text()))
server.send("request2");
else
server.send(frame.text());
});
});
```
```python async
def message_handler(server: WebSocketRoute, message: Union[str, bytes]):
if message == "request":
server.send("request2")
else:
server.send(message)
def handler(ws: WebSocketRoute):
server = ws.connect_to_server()
ws.on_message(lambda message: message_handler(server, message))
await page.route_web_socket("wss://example.com/ws", handler)
```
```python sync
def message_handler(server: WebSocketRoute, message: Union[str, bytes]):
if message == "request":
server.send("request2")
else:
server.send(message)
def handler(ws: WebSocketRoute):
server = ws.connect_to_server()
ws.on_message(lambda message: message_handler(server, message))
page.route_web_socket("wss://example.com/ws", handler)
```
```csharp
await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
var server = ws.ConnectToServer();
ws.OnMessage(frame => {
if (frame.Text == "request")
server.Send("request2");
else
server.Send(frame.Text);
});
});
```
For more details, see [WebSocketRoute].

View file

@ -723,7 +723,9 @@ Important notes:
## WebSockets ## WebSockets
Playwright supports [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) inspection out of the box. Every time a WebSocket is created, the [`event: Page.webSocket`] event is fired. This event contains the [WebSocket] instance for further web socket frames inspection: Playwright supports [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) inspection, mocking and modifying out of the box. See our [API mocking guide](./mock.md#mock-websockets) to learn how to mock WebSockets.
Every time a WebSocket is created, the [`event: Page.webSocket`] event is fired. This event contains the [WebSocket] instance for further web socket frames inspection:
```js ```js
page.on('websocket', ws => { page.on('websocket', ws => {

View file

@ -13,8 +13,8 @@ New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWe
```csharp ```csharp
await page.RouteWebSocketAsync("/ws", ws => { await page.RouteWebSocketAsync("/ws", ws => {
ws.OnMessage(message => { ws.OnMessage(frame => {
if (message == "request") if (frame.Text == "request")
ws.Send("response"); ws.Send("response");
}); });
}); });

View file

@ -12,8 +12,8 @@ New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWe
```java ```java
page.routeWebSocket("/ws", ws -> { page.routeWebSocket("/ws", ws -> {
ws.onMessage(message -> { ws.onMessage(frame -> {
if ("request".equals(message)) if ("request".equals(frame.text()))
ws.send("response"); ws.send("response");
}); });
}); });

View file

@ -8,6 +8,11 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
## Version 1.48 ## Version 1.48
<LiteYouTube
id="VGlkSBkMVCQ"
title="Playwright 1.48"
/>
### WebSocket routing ### WebSocket routing
New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWebSocket`] allow to intercept, modify and mock WebSocket connections initiated in the page. Below is a simple example that mocks WebSocket communication by responding to a `"request"` with a `"response"`. New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWebSocket`] allow to intercept, modify and mock WebSocket connections initiated in the page. Below is a simple example that mocks WebSocket communication by responding to a `"request"` with a `"response"`.

68
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "playwright-internal", "name": "playwright-internal",
"version": "1.48.0-next", "version": "1.48.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "playwright-internal", "name": "playwright-internal",
"version": "1.48.0-next", "version": "1.48.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@ -7925,10 +7925,10 @@
} }
}, },
"packages/playwright": { "packages/playwright": {
"version": "1.48.0-next", "version": "1.48.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.48.0-next" "playwright-core": "1.48.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7942,11 +7942,11 @@
}, },
"packages/playwright-browser-chromium": { "packages/playwright-browser-chromium": {
"name": "@playwright/browser-chromium", "name": "@playwright/browser-chromium",
"version": "1.48.0-next", "version": "1.48.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.48.0-next" "playwright-core": "1.48.2"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -7954,11 +7954,11 @@
}, },
"packages/playwright-browser-firefox": { "packages/playwright-browser-firefox": {
"name": "@playwright/browser-firefox", "name": "@playwright/browser-firefox",
"version": "1.48.0-next", "version": "1.48.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.48.0-next" "playwright-core": "1.48.2"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -7966,22 +7966,22 @@
}, },
"packages/playwright-browser-webkit": { "packages/playwright-browser-webkit": {
"name": "@playwright/browser-webkit", "name": "@playwright/browser-webkit",
"version": "1.48.0-next", "version": "1.48.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.48.0-next" "playwright-core": "1.48.2"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"packages/playwright-chromium": { "packages/playwright-chromium": {
"version": "1.48.0-next", "version": "1.48.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.48.0-next" "playwright-core": "1.48.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7991,7 +7991,7 @@
} }
}, },
"packages/playwright-core": { "packages/playwright-core": {
"version": "1.48.0-next", "version": "1.48.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
@ -8002,11 +8002,11 @@
}, },
"packages/playwright-ct-core": { "packages/playwright-ct-core": {
"name": "@playwright/experimental-ct-core", "name": "@playwright/experimental-ct-core",
"version": "1.48.0-next", "version": "1.48.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.48.0-next", "playwright": "1.48.2",
"playwright-core": "1.48.0-next", "playwright-core": "1.48.2",
"vite": "^5.2.8" "vite": "^5.2.8"
}, },
"engines": { "engines": {
@ -8015,10 +8015,10 @@
}, },
"packages/playwright-ct-react": { "packages/playwright-ct-react": {
"name": "@playwright/experimental-ct-react", "name": "@playwright/experimental-ct-react",
"version": "1.48.0-next", "version": "1.48.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.48.0-next", "@playwright/experimental-ct-core": "1.48.2",
"@vitejs/plugin-react": "^4.2.1" "@vitejs/plugin-react": "^4.2.1"
}, },
"bin": { "bin": {
@ -8030,10 +8030,10 @@
}, },
"packages/playwright-ct-react17": { "packages/playwright-ct-react17": {
"name": "@playwright/experimental-ct-react17", "name": "@playwright/experimental-ct-react17",
"version": "1.48.0-next", "version": "1.48.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.48.0-next", "@playwright/experimental-ct-core": "1.48.2",
"@vitejs/plugin-react": "^4.2.1" "@vitejs/plugin-react": "^4.2.1"
}, },
"bin": { "bin": {
@ -8045,10 +8045,10 @@
}, },
"packages/playwright-ct-solid": { "packages/playwright-ct-solid": {
"name": "@playwright/experimental-ct-solid", "name": "@playwright/experimental-ct-solid",
"version": "1.48.0-next", "version": "1.48.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.48.0-next", "@playwright/experimental-ct-core": "1.48.2",
"vite-plugin-solid": "^2.7.0" "vite-plugin-solid": "^2.7.0"
}, },
"bin": { "bin": {
@ -8063,10 +8063,10 @@
}, },
"packages/playwright-ct-svelte": { "packages/playwright-ct-svelte": {
"name": "@playwright/experimental-ct-svelte", "name": "@playwright/experimental-ct-svelte",
"version": "1.48.0-next", "version": "1.48.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.48.0-next", "@playwright/experimental-ct-core": "1.48.2",
"@sveltejs/vite-plugin-svelte": "^3.0.1" "@sveltejs/vite-plugin-svelte": "^3.0.1"
}, },
"bin": { "bin": {
@ -8081,10 +8081,10 @@
}, },
"packages/playwright-ct-vue": { "packages/playwright-ct-vue": {
"name": "@playwright/experimental-ct-vue", "name": "@playwright/experimental-ct-vue",
"version": "1.48.0-next", "version": "1.48.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.48.0-next", "@playwright/experimental-ct-core": "1.48.2",
"@vitejs/plugin-vue": "^4.2.1" "@vitejs/plugin-vue": "^4.2.1"
}, },
"bin": { "bin": {
@ -8096,10 +8096,10 @@
}, },
"packages/playwright-ct-vue2": { "packages/playwright-ct-vue2": {
"name": "@playwright/experimental-ct-vue2", "name": "@playwright/experimental-ct-vue2",
"version": "1.48.0-next", "version": "1.48.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.48.0-next", "@playwright/experimental-ct-core": "1.48.2",
"@vitejs/plugin-vue2": "^2.2.0" "@vitejs/plugin-vue2": "^2.2.0"
}, },
"bin": { "bin": {
@ -8148,11 +8148,11 @@
} }
}, },
"packages/playwright-firefox": { "packages/playwright-firefox": {
"version": "1.48.0-next", "version": "1.48.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.48.0-next" "playwright-core": "1.48.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8163,10 +8163,10 @@
}, },
"packages/playwright-test": { "packages/playwright-test": {
"name": "@playwright/test", "name": "@playwright/test",
"version": "1.48.0-next", "version": "1.48.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.48.0-next" "playwright": "1.48.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8176,11 +8176,11 @@
} }
}, },
"packages/playwright-webkit": { "packages/playwright-webkit": {
"version": "1.48.0-next", "version": "1.48.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.48.0-next" "playwright-core": "1.48.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"

View file

@ -1,7 +1,7 @@
{ {
"name": "playwright-internal", "name": "playwright-internal",
"private": true, "private": true,
"version": "1.48.0-next", "version": "1.48.2",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",

View file

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

View file

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

View file

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

View file

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

View file

@ -3,9 +3,9 @@
"browsers": [ "browsers": [
{ {
"name": "chromium", "name": "chromium",
"revision": "1137", "revision": "1140",
"installByDefault": true, "installByDefault": true,
"browserVersion": "130.0.6723.19" "browserVersion": "130.0.6723.31"
}, },
{ {
"name": "chromium-tip-of-tree", "name": "chromium-tip-of-tree",
@ -15,9 +15,9 @@
}, },
{ {
"name": "firefox", "name": "firefox",
"revision": "1464", "revision": "1465",
"installByDefault": true, "installByDefault": true,
"browserVersion": "130.0" "browserVersion": "131.0"
}, },
{ {
"name": "firefox-beta", "name": "firefox-beta",

View file

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

View file

@ -554,6 +554,7 @@ async function open(options: Options, url: string | undefined, language: string)
contextOptions, contextOptions,
device: options.device, device: options.device,
saveStorage: options.saveStorage, saveStorage: options.saveStorage,
handleSIGINT: false,
}); });
await openPage(context, url); await openPage(context, url);
} }
@ -577,6 +578,7 @@ async function codegen(options: Options & { target: string, output?: string, tes
codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions', 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,
handleSIGINT: false,
}); });
await openPage(context, url); await openPage(context, url);
} }

View file

@ -462,6 +462,7 @@ export class WebSocketRoute extends ChannelOwner<channels.WebSocketRouteChannel>
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.WebSocketRouteInitializer) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.WebSocketRouteInitializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
this.markAsInternalType();
this._server = { this._server = {
onMessage: (handler: (message: string | Buffer) => any) => { onMessage: (handler: (message: string | Buffer) => any) => {

View file

@ -976,6 +976,7 @@ scheme.BrowserContextEnableRecorderParams = tObject({
device: tOptional(tString), device: tOptional(tString),
saveStorage: tOptional(tString), saveStorage: tOptional(tString),
outputFile: tOptional(tString), outputFile: tOptional(tString),
handleSIGINT: tOptional(tBoolean),
omitCallTracking: tOptional(tBoolean), omitCallTracking: tOptional(tBoolean),
}); });
scheme.BrowserContextEnableRecorderResult = tOptional(tObject({})); scheme.BrowserContextEnableRecorderResult = tOptional(tObject({}));

View file

@ -37,7 +37,8 @@ export const chromiumSwitches = [
// PaintHolding - https://github.com/microsoft/playwright/issues/28023 // PaintHolding - https://github.com/microsoft/playwright/issues/28023
// ThirdPartyStoragePartitioning - https://github.com/microsoft/playwright/issues/32230 // ThirdPartyStoragePartitioning - https://github.com/microsoft/playwright/issues/32230
// LensOverlay - Hides the Lens feature in the URL address bar. Its not working in unofficial builds. // LensOverlay - Hides the Lens feature in the URL address bar. Its not working in unofficial builds.
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,ThirdPartyStoragePartitioning,LensOverlay', // PlzDedicatedWorker - https://github.com/microsoft/playwright/issues/31747
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,ThirdPartyStoragePartitioning,LensOverlay,PlzDedicatedWorker',
'--allow-pre-commit-input', '--allow-pre-commit-input',
'--disable-hang-monitor', '--disable-hang-monitor',
'--disable-ipc-flooding-protection', '--disable-ipc-flooding-protection',

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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.31 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/130.0.6723.19 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.31 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/130.0.6723.19 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.31 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/130.0.6723.19 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.31 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/130.0.6723.19 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.31 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/130.0.6723.19 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.31 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/130.0.6723.19 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.31 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/130.0.6723.19 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.31 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/130.0.6723.19 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.31 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/130.0.6723.19 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.31 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/130.0.6723.19 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.31 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/130.0.6723.19 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.31 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/130.0.6723.19 Safari/537.36 Edg/130.0.6723.19", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.31 Safari/537.36 Edg/130.0.6723.31",
"screen": { "screen": {
"width": 1792, "width": 1792,
"height": 1120 "height": 1120
@ -1592,7 +1592,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Firefox HiDPI": { "Desktop Firefox HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
"screen": { "screen": {
"width": 1792, "width": 1792,
"height": 1120 "height": 1120
@ -1622,7 +1622,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Desktop Chrome": { "Desktop Chrome": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.19 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.31 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/130.0.6723.19 Safari/537.36 Edg/130.0.6723.19", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.31 Safari/537.36 Edg/130.0.6723.31",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080
@ -1652,7 +1652,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Firefox": { "Desktop Firefox": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080

View file

@ -25,7 +25,7 @@ import zlib from 'zlib';
import type { HTTPCredentials } from '../../types/types'; import type { HTTPCredentials } from '../../types/types';
import { TimeoutSettings } from '../common/timeoutSettings'; import { TimeoutSettings } from '../common/timeoutSettings';
import { getUserAgent } from '../utils/userAgent'; import { getUserAgent } from '../utils/userAgent';
import { assert, createGuid, monotonicTime } from '../utils'; import { assert, createGuid, eventsHelper, monotonicTime, type RegisteredListener } from '../utils';
import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle'; import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle';
import { BrowserContext, verifyClientCertificates } from './browserContext'; import { BrowserContext, verifyClientCertificates } from './browserContext';
import { CookieStore, domainMatches, parseRawCookie } from './cookieStore'; import { CookieStore, domainMatches, parseRawCookie } from './cookieStore';
@ -312,8 +312,11 @@ export abstract class APIRequestContext extends SdkObject {
let securityDetails: har.SecurityDetails | undefined; let securityDetails: har.SecurityDetails | undefined;
const listeners: RegisteredListener[] = [];
const request = requestConstructor(url, requestOptions as any, async response => { const request = requestConstructor(url, requestOptions as any, async response => {
const responseAt = monotonicTime(); const responseAt = monotonicTime();
const notifyRequestFinished = (body?: Buffer) => { const notifyRequestFinished = (body?: Buffer) => {
const endAt = monotonicTime(); const endAt = monotonicTime();
// spec: http://www.softwareishard.com/blog/har-12-spec/#timings // spec: http://www.softwareishard.com/blog/har-12-spec/#timings
@ -478,12 +481,13 @@ export abstract class APIRequestContext extends SdkObject {
}); });
request.on('error', reject); request.on('error', reject);
const disposeListener = () => { listeners.push(
reject(new Error('Request context disposed.')); eventsHelper.addEventListener(this, APIRequestContext.Events.Dispose, () => {
request.destroy(); reject(new Error('Request context disposed.'));
}; request.destroy();
this.on(APIRequestContext.Events.Dispose, disposeListener); })
request.on('close', () => this.off(APIRequestContext.Events.Dispose, disposeListener)); );
request.on('close', () => eventsHelper.removeEventListeners(listeners));
request.on('socket', socket => { request.on('socket', socket => {
// happy eyeballs don't emit lookup and connect events, so we use our custom ones // happy eyeballs don't emit lookup and connect events, so we use our custom ones
@ -492,22 +496,24 @@ export abstract class APIRequestContext extends SdkObject {
tcpConnectionAt = happyEyeBallsTimings.tcpConnectionAt; tcpConnectionAt = happyEyeBallsTimings.tcpConnectionAt;
// non-happy-eyeballs sockets // non-happy-eyeballs sockets
socket.on('lookup', () => { dnsLookupAt = monotonicTime(); }); listeners.push(
socket.on('connect', () => { tcpConnectionAt = monotonicTime(); }); eventsHelper.addEventListener(socket, 'lookup', () => { dnsLookupAt = monotonicTime(); }),
socket.on('secureConnect', () => { eventsHelper.addEventListener(socket, 'connect', () => { tcpConnectionAt = monotonicTime(); }),
tlsHandshakeAt = monotonicTime(); eventsHelper.addEventListener(socket, 'secureConnect', () => {
tlsHandshakeAt = monotonicTime();
if (socket instanceof TLSSocket) { if (socket instanceof TLSSocket) {
const peerCertificate = socket.getPeerCertificate(); const peerCertificate = socket.getPeerCertificate();
securityDetails = { securityDetails = {
protocol: socket.getProtocol() ?? undefined, protocol: socket.getProtocol() ?? undefined,
subjectName: peerCertificate.subject.CN, subjectName: peerCertificate.subject.CN,
validFrom: new Date(peerCertificate.valid_from).getTime() / 1000, validFrom: new Date(peerCertificate.valid_from).getTime() / 1000,
validTo: new Date(peerCertificate.valid_to).getTime() / 1000, validTo: new Date(peerCertificate.valid_to).getTime() / 1000,
issuer: peerCertificate.issuer.CN issuer: peerCertificate.issuer.CN
}; };
} }
}); }),
);
serverIPAddress = socket.remoteAddress; serverIPAddress = socket.remoteAddress;
serverPort = socket.remotePort; serverPort = socket.remotePort;

View file

@ -147,13 +147,19 @@ export class InjectedScript {
builtinSetTimeout(callback: Function, timeout: number) { builtinSetTimeout(callback: Function, timeout: number) {
if (this.window.__pwClock?.builtin) if (this.window.__pwClock?.builtin)
return this.window.__pwClock.builtin.setTimeout(callback, timeout); return this.window.__pwClock.builtin.setTimeout(callback, timeout);
return setTimeout(callback, timeout); return this.window.setTimeout(callback, timeout);
}
builtinClearTimeout(timeout: number | undefined) {
if (this.window.__pwClock?.builtin)
return this.window.__pwClock.builtin.clearTimeout(timeout);
return this.window.clearTimeout(timeout);
} }
builtinRequestAnimationFrame(callback: FrameRequestCallback) { builtinRequestAnimationFrame(callback: FrameRequestCallback) {
if (this.window.__pwClock?.builtin) if (this.window.__pwClock?.builtin)
return this.window.__pwClock.builtin.requestAnimationFrame(callback); return this.window.__pwClock.builtin.requestAnimationFrame(callback);
return requestAnimationFrame(callback); return this.window.requestAnimationFrame(callback);
} }
eval(expression: string): any { eval(expression: string): any {
@ -1543,6 +1549,7 @@ declare global {
__pwClock?: { __pwClock?: {
builtin: { builtin: {
setTimeout: Window['setTimeout'], setTimeout: Window['setTimeout'],
clearTimeout: Window['clearTimeout'],
requestAnimationFrame: Window['requestAnimationFrame'], requestAnimationFrame: Window['requestAnimationFrame'],
} }
} }

View file

@ -1051,7 +1051,7 @@ export class Recorder {
recreationInterval = this.injectedScript.builtinSetTimeout(recreate, 500); recreationInterval = this.injectedScript.builtinSetTimeout(recreate, 500);
}; };
recreationInterval = this.injectedScript.builtinSetTimeout(recreate, 500); recreationInterval = this.injectedScript.builtinSetTimeout(recreate, 500);
this._listeners.push(() => clearInterval(recreationInterval)); this._listeners.push(() => this.injectedScript.builtinClearTimeout(recreationInterval));
this.overlay?.install(); this.overlay?.install();
this.document.adoptedStyleSheets.push(this._stylesheet); this.document.adoptedStyleSheets.push(this._stylesheet);

View file

@ -143,6 +143,7 @@ export function inject(globalThis: GlobalThis) {
this.url = typeof url === 'string' ? url : url.href; this.url = typeof url === 'string' ? url : url.href;
try { try {
this.url = new URL(url).href;
this._origin = new URL(url).origin; this._origin = new URL(url).origin;
} catch { } catch {
} }

View file

@ -34,6 +34,7 @@ import { buildFullSelector } from '../utils/isomorphic/recorderUtils';
const recorderSymbol = Symbol('recorderSymbol'); const recorderSymbol = Symbol('recorderSymbol');
export class Recorder implements InstrumentationListener, IRecorder { export class Recorder implements InstrumentationListener, IRecorder {
readonly handleSIGINT: boolean | undefined;
private _context: BrowserContext; private _context: BrowserContext;
private _mode: Mode; private _mode: Mode;
private _highlightedSelector = ''; private _highlightedSelector = '';
@ -75,6 +76,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) { constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
this._mode = params.mode || 'none'; this._mode = params.mode || 'none';
this.handleSIGINT = params.handleSIGINT;
this._contextRecorder = new ContextRecorder(codegenMode, context, params, {}); this._contextRecorder = new ContextRecorder(codegenMode, context, params, {});
this._context = context; this._context = context;
this._omitCallTracking = !!params.omitCallTracking; this._omitCallTracking = !!params.omitCallTracking;
@ -140,6 +142,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
this._contextRecorder.on(ContextRecorder.Events.Change, (data: { sources: Source[], actions: actions.ActionInContext[] }) => { this._contextRecorder.on(ContextRecorder.Events.Change, (data: { sources: Source[], actions: actions.ActionInContext[] }) => {
this._recorderSources = data.sources; this._recorderSources = data.sources;
recorderApp.setActions(data.actions, data.sources); recorderApp.setActions(data.actions, data.sources);
recorderApp.setRunningFile(undefined);
this._pushAllSources(); this._pushAllSources();
}); });
@ -299,7 +302,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
} }
this._pushAllSources(); this._pushAllSources();
if (fileToSelect) if (fileToSelect)
this._recorderApp?.setFile(fileToSelect); this._recorderApp?.setRunningFile(fileToSelect);
} }
private _pushAllSources() { private _pushAllSources() {

View file

@ -34,7 +34,7 @@ export class EmptyRecorderApp extends EventEmitter implements IRecorderApp {
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> {}
async setFile(file: string): Promise<void> {} async setRunningFile(file: string | undefined): Promise<void> {}
async setSelector(selector: string, userGesture?: boolean): Promise<void> {} async setSelector(selector: string, userGesture?: boolean): Promise<void> {}
async updateCallLogs(callLogs: CallLog[]): Promise<void> {} async updateCallLogs(callLogs: CallLog[]): Promise<void> {}
async setSources(sources: Source[]): Promise<void> {} async setSources(sources: Source[]): Promise<void> {}
@ -111,7 +111,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
noDefaultViewport: true, noDefaultViewport: true,
headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed), headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed),
useWebSocket: isUnderTest(), useWebSocket: isUnderTest(),
handleSIGINT: false, handleSIGINT: recorder.handleSIGINT,
executablePath: inspectedContext._browser.options.isChromium ? inspectedContext._browser.options.customExecutablePath : undefined, executablePath: inspectedContext._browser.options.isChromium ? inspectedContext._browser.options.customExecutablePath : undefined,
} }
}); });
@ -131,9 +131,9 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
}).toString(), { isFunction: true }, mode).catch(() => {}); }).toString(), { isFunction: true }, mode).catch(() => {});
} }
async setFile(file: string): Promise<void> { async setRunningFile(file: string | undefined): Promise<void> {
await this._page.mainFrame().evaluateExpression(((file: string) => { await this._page.mainFrame().evaluateExpression(((file: string) => {
window.playwrightSetFile(file); window.playwrightSetRunningFile(file);
}).toString(), { isFunction: true }, file).catch(() => {}); }).toString(), { isFunction: true }, file).catch(() => {});
} }

View file

@ -126,6 +126,8 @@ export class RecorderCollection extends EventEmitter {
} }
private _fireChange() { private _fireChange() {
if (!this._enabled)
return;
this.emit('change', collapseActions(this._actions)); this.emit('change', collapseActions(this._actions));
} }
} }

View file

@ -21,6 +21,7 @@ import type { EventEmitter } from 'events';
export interface IRecorder { export interface IRecorder {
setMode(mode: Mode): void; setMode(mode: Mode): void;
mode(): Mode; mode(): Mode;
readonly handleSIGINT: boolean | undefined;
} }
export interface IRecorderApp extends EventEmitter { export interface IRecorderApp extends EventEmitter {
@ -28,7 +29,7 @@ export interface IRecorderApp extends EventEmitter {
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>;
setFile(file: string): Promise<void>; setRunningFile(file: string | undefined): Promise<void>;
setSelector(selector: string, userGesture?: boolean): Promise<void>; setSelector(selector: string, userGesture?: boolean): Promise<void>;
updateCallLogs(callLogs: CallLog[]): Promise<void>; updateCallLogs(callLogs: CallLog[]): Promise<void>;
setSources(sources: Source[]): Promise<void>; setSources(sources: Source[]): Promise<void>;

View file

@ -66,8 +66,8 @@ export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp
this._transport.deliverEvent('setMode', { mode }); this._transport.deliverEvent('setMode', { mode });
} }
async setFile(file: string): Promise<void> { async setRunningFile(file: string | undefined): Promise<void> {
this._transport.deliverEvent('setFileIfNeeded', { file }); this._transport.deliverEvent('setRunningFile', { file });
} }
async setSelector(selector: string, userGesture?: boolean): Promise<void> { async setSelector(selector: string, userGesture?: boolean): Promise<void> {

View file

@ -38,8 +38,6 @@ const BIN_PATH = path.join(__dirname, '..', '..', '..', 'bin');
const PLAYWRIGHT_CDN_MIRRORS = [ const PLAYWRIGHT_CDN_MIRRORS = [
'https://playwright.azureedge.net', 'https://playwright.azureedge.net',
'https://playwright-akamai.azureedge.net',
'https://playwright-verizon.azureedge.net',
]; ];
if (process.env.PW_TEST_CDN_THAT_SHOULD_WORK) { if (process.env.PW_TEST_CDN_THAT_SHOULD_WORK) {

View file

@ -15355,7 +15355,7 @@ export interface CDPSession {
* the WebSocket. Here is an example that responds to a `"request"` with a `"response"`. * the WebSocket. Here is an example that responds to a `"request"` with a `"response"`.
* *
* ```js * ```js
* await page.routeWebSocket('/ws', ws => { * await page.routeWebSocket('wss://example.com/ws', ws => {
* ws.onMessage(message => { * ws.onMessage(message => {
* if (message === 'request') * if (message === 'request')
* ws.send('response'); * ws.send('response');
@ -15368,6 +15368,18 @@ export interface CDPSession {
* inside the WebSocket route handler, Playwright assumes that WebSocket will be mocked, and opens the WebSocket * inside the WebSocket route handler, Playwright assumes that WebSocket will be mocked, and opens the WebSocket
* inside the page automatically. * inside the page automatically.
* *
* Here is another example that handles JSON messages:
*
* ```js
* await page.routeWebSocket('wss://example.com/ws', ws => {
* ws.onMessage(message => {
* const json = JSON.parse(message);
* if (json.request === 'question')
* ws.send(JSON.stringify({ response: 'answer' }));
* });
* });
* ```
*
* **Intercepting** * **Intercepting**
* *
* Alternatively, you may want to connect to the actual server, but intercept messages in-between and modify or block * Alternatively, you may want to connect to the actual server, but intercept messages in-between and modify or block

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-core", "name": "@playwright/experimental-ct-core",
"version": "1.48.0-next", "version": "1.48.2",
"description": "Playwright Component Testing Helpers", "description": "Playwright Component Testing Helpers",
"repository": { "repository": {
"type": "git", "type": "git",
@ -26,8 +26,8 @@
} }
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.48.0-next", "playwright-core": "1.48.2",
"vite": "^5.2.8", "vite": "^5.2.8",
"playwright": "1.48.0-next" "playwright": "1.48.2"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-react", "name": "@playwright/experimental-ct-react",
"version": "1.48.0-next", "version": "1.48.2",
"description": "Playwright Component Testing for React", "description": "Playwright Component Testing for React",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.48.0-next", "@playwright/experimental-ct-core": "1.48.2",
"@vitejs/plugin-react": "^4.2.1" "@vitejs/plugin-react": "^4.2.1"
}, },
"bin": { "bin": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-react17", "name": "@playwright/experimental-ct-react17",
"version": "1.48.0-next", "version": "1.48.2",
"description": "Playwright Component Testing for React", "description": "Playwright Component Testing for React",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.48.0-next", "@playwright/experimental-ct-core": "1.48.2",
"@vitejs/plugin-react": "^4.2.1" "@vitejs/plugin-react": "^4.2.1"
}, },
"bin": { "bin": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-solid", "name": "@playwright/experimental-ct-solid",
"version": "1.48.0-next", "version": "1.48.2",
"description": "Playwright Component Testing for Solid", "description": "Playwright Component Testing for Solid",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.48.0-next", "@playwright/experimental-ct-core": "1.48.2",
"vite-plugin-solid": "^2.7.0" "vite-plugin-solid": "^2.7.0"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-svelte", "name": "@playwright/experimental-ct-svelte",
"version": "1.48.0-next", "version": "1.48.2",
"description": "Playwright Component Testing for Svelte", "description": "Playwright Component Testing for Svelte",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.48.0-next", "@playwright/experimental-ct-core": "1.48.2",
"@sveltejs/vite-plugin-svelte": "^3.0.1" "@sveltejs/vite-plugin-svelte": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-vue", "name": "@playwright/experimental-ct-vue",
"version": "1.48.0-next", "version": "1.48.2",
"description": "Playwright Component Testing for Vue", "description": "Playwright Component Testing for Vue",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.48.0-next", "@playwright/experimental-ct-core": "1.48.2",
"@vitejs/plugin-vue": "^4.2.1" "@vitejs/plugin-vue": "^4.2.1"
}, },
"bin": { "bin": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-vue2", "name": "@playwright/experimental-ct-vue2",
"version": "1.48.0-next", "version": "1.48.2",
"description": "Playwright Component Testing for Vue2", "description": "Playwright Component Testing for Vue2",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.48.0-next", "@playwright/experimental-ct-core": "1.48.2",
"@vitejs/plugin-vue2": "^2.2.0" "@vitejs/plugin-vue2": "^2.2.0"
}, },
"devDependencies": { "devDependencies": {

View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/test", "name": "@playwright/test",
"version": "1.48.0-next", "version": "1.48.2",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,6 +30,6 @@
}, },
"scripts": {}, "scripts": {},
"dependencies": { "dependencies": {
"playwright": "1.48.0-next" "playwright": "1.48.2"
} }
} }

View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright", "name": "playwright",
"version": "1.48.0-next", "version": "1.48.2",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",
@ -56,7 +56,7 @@
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.48.0-next" "playwright-core": "1.48.2"
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "2.3.2" "fsevents": "2.3.2"

View file

@ -302,10 +302,11 @@ export class TestServerDispatcher implements TestServerInterface {
preserveOutputDir: true, preserveOutputDir: true,
reporter: params.reporters ? params.reporters.map(r => [r]) : undefined, reporter: params.reporters ? params.reporters.map(r => [r]) : undefined,
use: { use: {
...(this._configCLIOverrides.use || {}), ...this._configCLIOverrides.use,
trace: params.trace === 'on' ? { mode: 'on', sources: false, _live: true } : (params.trace === 'off' ? 'off' : undefined), ...(params.trace === 'on' ? { trace: { mode: 'on', sources: false, _live: true } } : {}),
video: params.video === 'on' ? 'on' : (params.video === 'off' ? 'off' : undefined), ...(params.trace === 'off' ? { trace: 'off' } : {}),
headless: params.headed ? false : undefined, ...(params.video === 'on' || params.video === 'off' ? { video: params.video } : {}),
...(params.headed !== undefined ? { headless: !params.headed } : {}),
_optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined, _optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined, _optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
}, },

View file

@ -1777,6 +1777,7 @@ export type BrowserContextEnableRecorderParams = {
device?: string, device?: string,
saveStorage?: string, saveStorage?: string,
outputFile?: string, outputFile?: string,
handleSIGINT?: boolean,
omitCallTracking?: boolean, omitCallTracking?: boolean,
}; };
export type BrowserContextEnableRecorderOptions = { export type BrowserContextEnableRecorderOptions = {
@ -1790,6 +1791,7 @@ export type BrowserContextEnableRecorderOptions = {
device?: string, device?: string,
saveStorage?: string, saveStorage?: string,
outputFile?: string, outputFile?: string,
handleSIGINT?: boolean,
omitCallTracking?: boolean, omitCallTracking?: boolean,
}; };
export type BrowserContextEnableRecorderResult = void; export type BrowserContextEnableRecorderResult = void;

View file

@ -1208,6 +1208,7 @@ BrowserContext:
device: string? device: string?
saveStorage: string? saveStorage: string?
outputFile: string? outputFile: string?
handleSIGINT: boolean?
omitCallTracking: boolean? omitCallTracking: boolean?
newCDPSession: newCDPSession:

View file

@ -41,13 +41,11 @@ export const Recorder: React.FC<RecorderProps> = ({
log, log,
mode, mode,
}) => { }) => {
const [fileId, setFileId] = React.useState<string | undefined>(); const [selectedFileId, setSelectedFileId] = React.useState<string | undefined>();
const [runningFileId, setRunningFileId] = React.useState<string | undefined>();
const [selectedTab, setSelectedTab] = React.useState<string>('log'); const [selectedTab, setSelectedTab] = React.useState<string>('log');
React.useEffect(() => { const fileId = selectedFileId || runningFileId || sources[0]?.id;
if (!fileId && sources.length > 0)
setFileId(sources[0].id);
}, [fileId, sources]);
const source = React.useMemo(() => { const source = React.useMemo(() => {
if (fileId) { if (fileId) {
@ -66,7 +64,7 @@ export const Recorder: React.FC<RecorderProps> = ({
setLocator(asLocator(language, selector)); setLocator(asLocator(language, selector));
}; };
window.playwrightSetFile = setFileId; window.playwrightSetRunningFile = setRunningFileId;
const messagesEndRef = React.useRef<HTMLDivElement>(null); const messagesEndRef = React.useRef<HTMLDivElement>(null);
React.useLayoutEffect(() => { React.useLayoutEffect(() => {
@ -134,19 +132,19 @@ export const Recorder: React.FC<RecorderProps> = ({
<ToolbarButton icon='files' title='Copy' disabled={!source || !source.text} onClick={() => { <ToolbarButton icon='files' title='Copy' disabled={!source || !source.text} onClick={() => {
copy(source.text); copy(source.text);
}}></ToolbarButton> }}></ToolbarButton>
<ToolbarButton icon='debug-continue' title='Resume (F8)' disabled={!paused} onClick={() => { <ToolbarButton icon='debug-continue' title='Resume (F8)' ariaLabel='Resume' disabled={!paused} onClick={() => {
window.dispatch({ event: 'resume' }); window.dispatch({ event: 'resume' });
}}></ToolbarButton> }}></ToolbarButton>
<ToolbarButton icon='debug-pause' title='Pause (F8)' disabled={paused} onClick={() => { <ToolbarButton icon='debug-pause' title='Pause (F8)' ariaLabel='Pause' disabled={paused} onClick={() => {
window.dispatch({ event: 'pause' }); window.dispatch({ event: 'pause' });
}}></ToolbarButton> }}></ToolbarButton>
<ToolbarButton icon='debug-step-over' title='Step over (F10)' disabled={!paused} onClick={() => { <ToolbarButton icon='debug-step-over' title='Step over (F10)' ariaLabel='Step over' disabled={!paused} onClick={() => {
window.dispatch({ event: 'step' }); window.dispatch({ event: 'step' });
}}></ToolbarButton> }}></ToolbarButton>
<div style={{ flex: 'auto' }}></div> <div style={{ flex: 'auto' }}></div>
<div>Target:</div> <div>Target:</div>
<SourceChooser fileId={fileId} sources={sources} setFileId={fileId => { <SourceChooser fileId={fileId} sources={sources} setFileId={fileId => {
setFileId(fileId); setSelectedFileId(fileId);
window.dispatch({ event: 'fileChanged', params: { file: fileId } }); window.dispatch({ event: 'fileChanged', params: { file: fileId } });
}} /> }} />
<ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => { <ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => {

View file

@ -96,7 +96,7 @@ declare global {
playwrightSetSources: (sources: Source[]) => void; playwrightSetSources: (sources: Source[]) => void;
playwrightSetOverlayVisible: (visible: boolean) => void; playwrightSetOverlayVisible: (visible: boolean) => void;
playwrightUpdateLogs: (callLogs: CallLog[]) => void; playwrightUpdateLogs: (callLogs: CallLog[]) => void;
playwrightSetFile: (file: string) => void; playwrightSetRunningFile: (file: string | undefined) => void;
playwrightSetSelector: (selector: string, focus?: boolean) => void; playwrightSetSelector: (selector: string, focus?: boolean) => void;
playwrightSourcesEchoForTest: Source[]; playwrightSourcesEchoForTest: Source[];
dispatch(data: any): Promise<void>; dispatch(data: any): Promise<void>;

View file

@ -0,0 +1,49 @@
/**
* 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 class LRUCache<K, V> {
private _maxSize: number;
private _map: Map<K, { value: V, size: number }>;
private _size: number;
constructor(maxSize: number) {
this._maxSize = maxSize;
this._map = new Map();
this._size = 0;
}
getOrCompute(key: K, compute: () => { value: V, size: number }): V {
if (this._map.has(key)) {
const result = this._map.get(key)!;
// reinserting makes this the least recently used entry
this._map.delete(key);
this._map.set(key, result);
return result.value;
}
const result = compute();
while (this._map.size && this._size + result.size > this._maxSize) {
const [firstKey, firstValue] = this._map.entries().next().value;
this._size -= firstValue.size;
this._map.delete(firstKey);
}
this._map.set(key, result);
this._size += result.size;
return result.value;
}
}

View file

@ -36,16 +36,16 @@ const scopePath = new URL(self.registration.scope).pathname;
const loadedTraces = new Map<string, { traceModel: TraceModel, snapshotServer: SnapshotServer }>(); const loadedTraces = new Map<string, { traceModel: TraceModel, snapshotServer: SnapshotServer }>();
const clientIdToTraceUrls = new Map<string, Set<string>>(); const clientIdToTraceUrls = new Map<string, { limit: number | undefined, traceUrls: Set<string> }>();
async function loadTrace(traceUrl: string, traceFileName: string | null, clientId: string, progress: (done: number, total: number) => undefined): Promise<TraceModel> { async function loadTrace(traceUrl: string, traceFileName: string | null, clientId: string, limit: number | undefined, progress: (done: number, total: number) => undefined): Promise<TraceModel> {
await gc(); await gc();
let set = clientIdToTraceUrls.get(clientId); let data = clientIdToTraceUrls.get(clientId);
if (!set) { if (!data) {
set = new Set(); data = { limit, traceUrls: new Set() };
clientIdToTraceUrls.set(clientId, set); clientIdToTraceUrls.set(clientId, data);
} }
set.add(traceUrl); data.traceUrls.add(traceUrl);
const traceModel = new TraceModel(); const traceModel = new TraceModel();
try { try {
@ -97,7 +97,8 @@ async function doFetch(event: FetchEvent): Promise<Response> {
if (relativePath === '/contexts') { if (relativePath === '/contexts') {
try { try {
const traceModel = await loadTrace(traceUrl!, url.searchParams.get('traceFileName'), event.clientId, (done: number, total: number) => { const limit = url.searchParams.has('limit') ? +url.searchParams.get('limit')! : undefined;
const traceModel = await loadTrace(traceUrl!, url.searchParams.get('traceFileName'), event.clientId, limit, (done: number, total: number) => {
client.postMessage({ method: 'progress', params: { done, total } }); client.postMessage({ method: 'progress', params: { done, total } });
}); });
return new Response(JSON.stringify(traceModel!.contextEntries), { return new Response(JSON.stringify(traceModel!.contextEntries), {
@ -172,12 +173,18 @@ async function gc() {
const clients = await self.clients.matchAll(); const clients = await self.clients.matchAll();
const usedTraces = new Set<string>(); const usedTraces = new Set<string>();
for (const [clientId, traceUrls] of clientIdToTraceUrls) { for (const [clientId, data] of clientIdToTraceUrls) {
// @ts-ignore // @ts-ignore
if (!clients.find(c => c.id === clientId)) if (!clients.find(c => c.id === clientId)) {
clientIdToTraceUrls.delete(clientId); clientIdToTraceUrls.delete(clientId);
else continue;
traceUrls.forEach(url => usedTraces.add(url)); }
if (data.limit !== undefined) {
const ordered = [...data.traceUrls];
// Leave the newest requested traces.
data.traceUrls = new Set(ordered.slice(ordered.length - data.limit));
}
data.traceUrls.forEach(url => usedTraces.add(url));
} }
for (const traceUrl of loadedTraces.keys()) { for (const traceUrl of loadedTraces.keys()) {

View file

@ -16,6 +16,7 @@
import { escapeHTMLAttribute, escapeHTML } from '@isomorphic/stringUtils'; import { escapeHTMLAttribute, escapeHTML } from '@isomorphic/stringUtils';
import type { FrameSnapshot, NodeNameAttributesChildNodesSnapshot, NodeSnapshot, RenderedFrameSnapshot, ResourceSnapshot, SubtreeReferenceSnapshot } from '@trace/snapshot'; import type { FrameSnapshot, NodeNameAttributesChildNodesSnapshot, NodeSnapshot, RenderedFrameSnapshot, ResourceSnapshot, SubtreeReferenceSnapshot } from '@trace/snapshot';
import type { LRUCache } from './lruCache';
function isNodeNameAttributesChildNodesSnapshot(n: NodeSnapshot): n is NodeNameAttributesChildNodesSnapshot { function isNodeNameAttributesChildNodesSnapshot(n: NodeSnapshot): n is NodeNameAttributesChildNodesSnapshot {
return Array.isArray(n) && typeof n[0] === 'string'; return Array.isArray(n) && typeof n[0] === 'string';
@ -25,35 +26,8 @@ function isSubtreeReferenceSnapshot(n: NodeSnapshot): n is SubtreeReferenceSnaps
return Array.isArray(n) && Array.isArray(n[0]); return Array.isArray(n) && Array.isArray(n[0]);
} }
let cacheSize = 0;
const cache = new Map<SnapshotRenderer, string>();
const CACHE_SIZE = 300_000_000; // 300mb
function lruCache(key: SnapshotRenderer, compute: () => string): string {
if (cache.has(key)) {
const value = cache.get(key)!;
// reinserting makes this the least recently used entry
cache.delete(key);
cache.set(key, value);
return value;
}
const result = compute();
while (cache.size && cacheSize + result.length > CACHE_SIZE) {
const [firstKey, firstValue] = cache.entries().next().value;
cacheSize -= firstValue.length;
cache.delete(firstKey);
}
cache.set(key, result);
cacheSize += result.length;
return result;
}
export class SnapshotRenderer { export class SnapshotRenderer {
private _htmlCache: LRUCache<SnapshotRenderer, string>;
private _snapshots: FrameSnapshot[]; private _snapshots: FrameSnapshot[];
private _index: number; private _index: number;
readonly snapshotName: string | undefined; readonly snapshotName: string | undefined;
@ -61,7 +35,8 @@ export class SnapshotRenderer {
private _snapshot: FrameSnapshot; private _snapshot: FrameSnapshot;
private _callId: string; private _callId: string;
constructor(resources: ResourceSnapshot[], snapshots: FrameSnapshot[], index: number) { constructor(htmlCache: LRUCache<SnapshotRenderer, string>, resources: ResourceSnapshot[], snapshots: FrameSnapshot[], index: number) {
this._htmlCache = htmlCache;
this._resources = resources; this._resources = resources;
this._snapshots = snapshots; this._snapshots = snapshots;
this._index = index; this._index = index;
@ -151,16 +126,15 @@ export class SnapshotRenderer {
}; };
const snapshot = this._snapshot; const snapshot = this._snapshot;
const html = lruCache(this, () => { const html = this._htmlCache.getOrCompute(this, () => {
visit(snapshot.html, this._index, undefined, undefined); visit(snapshot.html, this._index, undefined, undefined);
const html = result.join('');
// Hide the document in order to prevent flickering. We will unhide once script has processed shadow.
const prefix = snapshot.doctype ? `<!DOCTYPE ${snapshot.doctype}>` : ''; const prefix = snapshot.doctype ? `<!DOCTYPE ${snapshot.doctype}>` : '';
return prefix + [ const html = prefix + [
// Hide the document in order to prevent flickering. We will unhide once script has processed shadow.
'<style>*,*::before,*::after { visibility: hidden }</style>', '<style>*,*::before,*::after { visibility: hidden }</style>',
`<script>${snapshotScript(this._callId, this.snapshotName)}</script>` `<script>${snapshotScript(this._callId, this.snapshotName)}</script>`
].join('') + html; ].join('') + result.join('');
return { value: html, size: html.length };
}); });
return { html, pageId: snapshot.pageId, frameId: snapshot.frameId, index: this._index }; return { html, pageId: snapshot.pageId, frameId: snapshot.frameId, index: this._index };

View file

@ -16,6 +16,7 @@
import type { FrameSnapshot, ResourceSnapshot } from '@trace/snapshot'; import type { FrameSnapshot, ResourceSnapshot } from '@trace/snapshot';
import { rewriteURLForCustomProtocol, SnapshotRenderer } from './snapshotRenderer'; import { rewriteURLForCustomProtocol, SnapshotRenderer } from './snapshotRenderer';
import { LRUCache } from './lruCache';
export class SnapshotStorage { export class SnapshotStorage {
private _resources: ResourceSnapshot[] = []; private _resources: ResourceSnapshot[] = [];
@ -23,6 +24,7 @@ export class SnapshotStorage {
raw: FrameSnapshot[], raw: FrameSnapshot[],
renderers: SnapshotRenderer[] renderers: SnapshotRenderer[]
}>(); }>();
private _cache = new LRUCache<SnapshotRenderer, string>(100_000_000); // 100MB per each trace
addResource(resource: ResourceSnapshot): void { addResource(resource: ResourceSnapshot): void {
resource.request.url = rewriteURLForCustomProtocol(resource.request.url); resource.request.url = rewriteURLForCustomProtocol(resource.request.url);
@ -43,7 +45,7 @@ export class SnapshotStorage {
this._frameSnapshots.set(snapshot.pageId, frameSnapshots); this._frameSnapshots.set(snapshot.pageId, frameSnapshots);
} }
frameSnapshots.raw.push(snapshot); frameSnapshots.raw.push(snapshot);
const renderer = new SnapshotRenderer(this._resources, frameSnapshots.raw, frameSnapshots.raw.length - 1); const renderer = new SnapshotRenderer(this._cache, this._resources, frameSnapshots.raw, frameSnapshots.raw.length - 1);
frameSnapshots.renderers.push(renderer); frameSnapshots.renderers.push(renderer);
return renderer; return renderer;
} }

View file

@ -20,7 +20,7 @@ import { ImageDiffView } from '@web/shared/imageDiffView';
import type { MultiTraceModel } from './modelUtil'; import type { MultiTraceModel } from './modelUtil';
import { PlaceholderPanel } from './placeholderPanel'; import { PlaceholderPanel } from './placeholderPanel';
import type { AfterActionTraceEventAttachment } from '@trace/trace'; import type { AfterActionTraceEventAttachment } from '@trace/trace';
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper'; import { CodeMirrorWrapper, lineHeight } from '@web/components/codeMirrorWrapper';
import { isTextualMimeType } from '@isomorphic/mimeType'; import { isTextualMimeType } from '@isomorphic/mimeType';
import { Expandable } from '@web/components/expandable'; import { Expandable } from '@web/components/expandable';
import { linkifyText } from '@web/renderUtils'; import { linkifyText } from '@web/renderUtils';
@ -51,6 +51,11 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
} }
}, [expanded, attachmentText, placeholder, attachment]); }, [expanded, attachmentText, placeholder, attachment]);
const snippetHeight = React.useMemo(() => {
const lineCount = attachmentText ? attachmentText.split('\n').length : 0;
return Math.min(Math.max(5, lineCount), 20) * lineHeight;
}, [attachmentText]);
const title = <span style={{ marginLeft: 5 }}> const title = <span style={{ marginLeft: 5 }}>
{linkifyText(attachment.name)} {hasContent && <a style={{ marginLeft: 5 }} href={downloadURL(attachment)}>download</a>} {linkifyText(attachment.name)} {hasContent && <a style={{ marginLeft: 5 }} href={downloadURL(attachment)}>download</a>}
</span>; </span>;
@ -62,14 +67,16 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
<Expandable title={title} expanded={expanded} setExpanded={setExpanded} expandOnTitleClick={true}> <Expandable title={title} expanded={expanded} setExpanded={setExpanded} expandOnTitleClick={true}>
{placeholder && <i>{placeholder}</i>} {placeholder && <i>{placeholder}</i>}
</Expandable> </Expandable>
{expanded && attachmentText !== null && <CodeMirrorWrapper {expanded && attachmentText !== null && <div className='vbox' style={{ height: snippetHeight }}>
text={attachmentText} <CodeMirrorWrapper
readOnly text={attachmentText}
mimeType={attachment.contentType} readOnly
linkify={true} mimeType={attachment.contentType}
lineNumbers={true} linkify={true}
wrapLines={false}> lineNumbers={true}
</CodeMirrorWrapper>} wrapLines={false}>
</CodeMirrorWrapper>
</div>}
</>; </>;
}; };

View file

@ -65,6 +65,7 @@ export const EmbeddedWorkbenchLoader: React.FunctionComponent = () => {
const url = traceURLs[i]; const url = traceURLs[i];
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set('trace', url); params.set('trace', url);
params.set('limit', String(traceURLs.length));
const response = await fetch(`contexts?${params.toString()}`); const response = await fetch(`contexts?${params.toString()}`);
if (!response.ok) { if (!response.ok) {
setProcessingErrorMessage((await response.json()).error); setProcessingErrorMessage((await response.json()).error);

View file

@ -58,6 +58,7 @@ export const ModelProvider: React.FunctionComponent<React.PropsWithChildren<{
async function loadSingleTraceFile(url: string): Promise<{ model: MultiTraceModel, sha1: string }> { async function loadSingleTraceFile(url: string): Promise<{ model: MultiTraceModel, sha1: string }> {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set('trace', url); params.set('trace', url);
params.set('limit', '1');
const response = await fetch(`contexts?${params.toString()}`); const response = await fetch(`contexts?${params.toString()}`);
const contextEntries = await response.json() as ContextEntry[]; const contextEntries = await response.json() as ContextEntry[];

View file

@ -20,7 +20,7 @@ import type { ActionTraceEvent } from '@trace/trace';
import { context, type MultiTraceModel, pageForAction, prevInList } from './modelUtil'; import { context, type MultiTraceModel, pageForAction, prevInList } from './modelUtil';
import { Toolbar } from '@web/components/toolbar'; import { Toolbar } from '@web/components/toolbar';
import { ToolbarButton } from '@web/components/toolbarButton'; import { ToolbarButton } from '@web/components/toolbarButton';
import { clsx, useMeasure, useSetting } from '@web/uiUtils'; import { clsx, useMeasure } from '@web/uiUtils';
import { InjectedScript } from '@injected/injectedScript'; import { InjectedScript } from '@injected/injectedScript';
import { Recorder } from '@injected/recorder/recorder'; import { Recorder } from '@injected/recorder/recorder';
import ConsoleAPI from '@injected/consoleApi'; import ConsoleAPI from '@injected/consoleApi';
@ -52,7 +52,7 @@ export const SnapshotTabsView: React.FunctionComponent<{
openPage?: (url: string, target?: string) => Window | any, openPage?: (url: string, target?: string) => Window | any,
}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator, openPage }) => { }> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator, openPage }) => {
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action'); const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
const [showScreenshotInsteadOfSnapshot] = useSetting('screenshot-instead-of-snapshot', false); const showScreenshotInsteadOfSnapshot = false;
const snapshots = React.useMemo(() => { const snapshots = React.useMemo(() => {
return collectSnapshots(action); return collectSnapshots(action);
@ -315,6 +315,10 @@ function createRecorders(recorders: { recorder: Recorder, frameSelector: string
const recorder = new Recorder(injectedScript); const recorder = new Recorder(injectedScript);
win._injectedScript = injectedScript; win._injectedScript = injectedScript;
win._recorder = { recorder, frameSelector: parentFrameSelector }; win._recorder = { recorder, frameSelector: parentFrameSelector };
if (isUnderTest) {
(window as any)._weakRecordersForTest = (window as any)._weakRecordersForTest || new Set();
(window as any)._weakRecordersForTest.add(new WeakRef(recorder));
}
} }
recorders.push(win._recorder); recorders.push(win._recorder);

View file

@ -111,6 +111,7 @@ const outputDirForTestCase = (testCase: reporterTypes.TestCase): string | undefi
async function loadSingleTraceFile(url: string): Promise<MultiTraceModel> { async function loadSingleTraceFile(url: string): Promise<MultiTraceModel> {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set('trace', url); params.set('trace', url);
params.set('limit', '1');
const response = await fetch(`contexts?${params.toString()}`); const response = await fetch(`contexts?${params.toString()}`);
const contextEntries = await response.json() as ContextEntry[]; const contextEntries = await response.json() as ContextEntry[];
return new MultiTraceModel(contextEntries); return new MultiTraceModel(contextEntries);

View file

@ -41,7 +41,6 @@ import type { Entry } from '@trace/har';
import './workbench.css'; import './workbench.css';
import { testStatusIcon, testStatusText } from './testUtils'; import { testStatusIcon, testStatusText } from './testUtils';
import type { UITestStatus } from './testUtils'; import type { UITestStatus } from './testUtils';
import { SettingsView } from './settingsView';
export const Workbench: React.FunctionComponent<{ export const Workbench: React.FunctionComponent<{
model?: modelUtil.MultiTraceModel, model?: modelUtil.MultiTraceModel,
@ -69,7 +68,7 @@ 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 [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false); const showScreenshot = false;
const setSelectedAction = React.useCallback((action: modelUtil.ActionTraceEventInContext | undefined) => { const setSelectedAction = React.useCallback((action: modelUtil.ActionTraceEventInContext | undefined) => {
setSelectedCallId(action?.callId); setSelectedCallId(action?.callId);
@ -310,13 +309,6 @@ export const Workbench: React.FunctionComponent<{
title: 'Metadata', title: 'Metadata',
component: <MetadataView model={model}/> component: <MetadataView model={model}/>
}; };
const settingsTab: TabbedPaneTabModel = {
id: 'settings',
title: 'Settings',
component: <SettingsView settings={[
{ 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' } : {})}>
{!hideTimeline && <Timeline {!hideTimeline && <Timeline
@ -351,8 +343,7 @@ export const Workbench: React.FunctionComponent<{
openPage={openPage} />} openPage={openPage} />}
sidebar={ sidebar={
<TabbedPane <TabbedPane
// Hide settings tab for now, it only includes screenshots as snapshots option which is not ready yet. tabs={[actionsTab, metadataTab]}
tabs={(showSettings && false) ? [actionsTab, metadataTab, settingsTab] : [actionsTab, metadataTab]}
selectedTab={selectedNavigatorTab} selectedTab={selectedNavigatorTab}
setSelectedTab={setSelectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}
/> />

View file

@ -131,6 +131,7 @@ export const WorkbenchLoader: React.FunctionComponent<{
params.set('trace', url); params.set('trace', url);
if (uploadedTraceNames.length) if (uploadedTraceNames.length)
params.set('traceFileName', uploadedTraceNames[i]); params.set('traceFileName', uploadedTraceNames[i]);
params.set('limit', String(traceURLs.length));
const response = await fetch(`contexts?${params.toString()}`); const response = await fetch(`contexts?${params.toString()}`);
if (!response.ok) { if (!response.ok) {
if (!isServer) if (!isServer)

View file

@ -28,6 +28,8 @@ export type SourceHighlight = {
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl' | 'html' | 'css' | 'markdown'; export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl' | 'html' | 'css' | 'markdown';
export const lineHeight = 20;
export interface SourceProps { export interface SourceProps {
text: string; text: string;
language?: Language; language?: Language;

View file

@ -22,7 +22,7 @@ export const SourceChooser: React.FC<{
fileId: string | undefined, fileId: string | undefined,
setFileId: (fileId: string) => void, setFileId: (fileId: string) => void,
}> = ({ sources, fileId, setFileId }) => { }> = ({ sources, fileId, setFileId }) => {
return <select className='source-chooser' hidden={!sources.length} value={fileId} onChange={event => { return <select className='source-chooser' hidden={!sources.length} title='Source chooser' value={fileId} onChange={event => {
setFileId(event.target.selectedOptions[0].value); setFileId(event.target.selectedOptions[0].value);
}}>{renderSourceOptions(sources)}</select>; }}>{renderSourceOptions(sources)}</select>;
}; };
@ -33,17 +33,21 @@ function renderSourceOptions(sources: Source[]): React.ReactNode {
<option key={source.id} value={source.id}>{transformTitle(source.label)}</option> <option key={source.id} value={source.id}>{transformTitle(source.label)}</option>
); );
const hasGroup = sources.some(s => s.group); const sourcesByGroups = new Map<string, Source[]>();
if (hasGroup) { for (const source of sources) {
const groups = new Set(sources.map(s => s.group)); let list = sourcesByGroups.get(source.group || 'Debugger');
return [...groups].filter(Boolean).map(group => ( if (!list) {
<optgroup label={group} key={group}> list = [];
{sources.filter(s => s.group === group).map(source => renderOption(source))} sourcesByGroups.set(source.group || 'Debugger', list);
</optgroup> }
)); list.push(source);
} }
return sources.map(source => renderOption(source)); return [...sourcesByGroups.entries()].map(([group, sources]) => (
<optgroup label={group} key={group}>
{sources.filter(s => (s.group || 'Debugger') === group).map(source => renderOption(source))}
</optgroup>
));
} }
export function emptySource(): Source { export function emptySource(): Source {

View file

@ -28,6 +28,7 @@ export interface ToolbarButtonProps {
style?: React.CSSProperties, style?: React.CSSProperties,
testId?: string, testId?: string,
className?: string, className?: string,
ariaLabel?: string,
} }
export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({
@ -40,6 +41,7 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
style, style,
testId, testId,
className, className,
ariaLabel,
}) => { }) => {
return <button return <button
className={clsx(className, 'toolbar-button', icon, toggled && 'toggled')} className={clsx(className, 'toolbar-button', icon, toggled && 'toggled')}
@ -50,6 +52,7 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
disabled={!!disabled} disabled={!!disabled}
style={style} style={style}
data-testid={testId} data-testid={testId}
aria-label={ariaLabel}
> >
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>} {icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
{children} {children}

View file

@ -20,8 +20,6 @@ import type { AddressInfo } from 'net';
const CDNS = [ const CDNS = [
'https://playwright.azureedge.net', 'https://playwright.azureedge.net',
'https://playwright-akamai.azureedge.net',
'https://playwright-verizon.azureedge.net',
]; ];
const DL_STAT_BLOCK = /^.*from url: (.*)$\n^.*to location: (.*)$\n^.*response status code: (.*)$\n^.*total bytes: (\d+)$\n^.*download complete, size: (\d+)$\n^.*SUCCESS downloading (\w+) .*$/gm; const DL_STAT_BLOCK = /^.*from url: (.*)$\n^.*to location: (.*)$\n^.*response status code: (.*)$\n^.*total bytes: (\d+)$\n^.*download complete, size: (\d+)$\n^.*SUCCESS downloading (\w+) .*$/gm;

View file

@ -188,9 +188,9 @@ test('test', async ({ page }) => {
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
});` });`
}); });
const length = events.length;
// No events after mode disabled // No events after mode disabled
await backend.setRecorderMode({ mode: 'none' }); await backend.setRecorderMode({ mode: 'none' });
const length = events.length;
await page.getByRole('button').click(); await page.getByRole('button').click();
expect(events).toHaveLength(length); expect(events).toHaveLength(length);
}); });

View file

@ -171,6 +171,13 @@ export class Recorder {
return this.page.locator('x-pw-tooltip').textContent(); return this.page.locator('x-pw-tooltip').textContent();
} }
async waitForHighlightNoTooltip(action: () => Promise<void>): Promise<string> {
await this.page.$$eval('x-pw-highlight', els => els.forEach(e => e.remove()));
await action();
await this.page.locator('x-pw-highlight').waitFor();
return '';
}
async waitForActionPerformed(): Promise<{ hovered: string | null, active: string | null }> { async waitForActionPerformed(): Promise<{ hovered: string | null, active: string | null }> {
let callback; let callback;
const listener = async msg => { const listener = async msg => {
@ -185,8 +192,8 @@ export class Recorder {
return new Promise(f => callback = f); return new Promise(f => callback = f);
} }
async hoverOverElement(selector: string, options?: { position?: { x: number, y: number }}): Promise<string> { async hoverOverElement(selector: string, options?: { position?: { x: number, y: number }, omitTooltip?: boolean }): Promise<string> {
return this.waitForHighlight(async () => { return (options?.omitTooltip ? this.waitForHighlightNoTooltip : this.waitForHighlight).call(this, async () => {
const box = await this.page.locator(selector).first().boundingBox(); const box = await this.page.locator(selector).first().boundingBox();
const offset = options?.position || { x: box.width / 2, y: box.height / 2 }; const offset = options?.position || { x: box.width / 2, y: box.height / 2 };
await this.page.mouse.move(box.x + offset.x, box.y + offset.y); await this.page.mouse.move(box.x + offset.x, box.y + offset.y);

View file

@ -15,7 +15,7 @@
*/ */
import type { Page } from 'playwright-core'; import type { Page } from 'playwright-core';
import { test as it, expect } from './inspectorTest'; import { test as it, expect, Recorder } from './inspectorTest';
import { waitForTestLog } from '../../config/utils'; import { waitForTestLog } from '../../config/utils';
@ -103,6 +103,7 @@ it.describe('pause', () => {
await page.pause(); await page.pause();
})(); })();
const recorderPage = await recorderPageGetter(); const recorderPage = await recorderPageGetter();
await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue(/pause\.spec\.ts/);
const source = await recorderPage.textContent('.source-line-paused'); const source = await recorderPage.textContent('.source-line-paused');
expect(source).toContain('page.pause()'); expect(source).toContain('page.pause()');
await recorderPage.click('[title="Resume (F8)"]'); await recorderPage.click('[title="Resume (F8)"]');
@ -480,6 +481,26 @@ it.describe('pause', () => {
await recorderPage.click('[title="Resume (F8)"]'); await recorderPage.click('[title="Resume (F8)"]');
await scriptPromise; await scriptPromise;
}); });
it('should record from debugger', async ({ page, recorderPageGetter }) => {
await page.setContent('<body style="width: 100%; height: 100%"></body>');
const scriptPromise = (async () => {
await page.pause();
})();
const recorderPage = await recorderPageGetter();
await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue(/pause\.spec\.ts/);
await expect(recorderPage.locator('.source-line-paused')).toHaveText(/await page\.pause\(\)/);
await recorderPage.getByRole('button', { name: 'Record' }).click();
const recorder = new Recorder(page, recorderPage);
await recorder.hoverOverElement('body', { omitTooltip: true });
await recorder.trustedClick();
await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue('javascript');
await expect(recorderPage.locator('.cm-wrapper')).toContainText(`await page.locator('body').click();`);
await recorderPage.getByRole('button', { name: 'Resume' }).click();
await scriptPromise;
});
}); });
async function sanitizeLog(recorderPage: Page): Promise<string[]> { async function sanitizeLog(recorderPage: Page): Promise<string[]> {

View file

@ -508,3 +508,27 @@ test('should throw when connecting twice', async ({ page, server }) => {
const error = await promise; const error = await promise;
expect(error.message).toContain('Already connected to the server'); expect(error.message).toContain('Already connected to the server');
}); });
test('should work with no trailing slash', async ({ page, server }) => {
const log: string[] = [];
// No trailing slash!
await page.routeWebSocket('ws://localhost:' + server.PORT, ws => {
ws.onMessage(message => {
log.push(message as string);
ws.send('response');
});
});
await page.goto('about:blank');
await page.evaluate(({ port }) => {
window.log = [];
// No trailing slash!
window.ws = new WebSocket('ws://localhost:' + port);
window.ws.addEventListener('message', event => window.log.push(event.data));
}, { port: server.PORT });
await expect.poll(() => page.evaluate(() => window.ws.readyState)).toBe(1);
await page.evaluate(() => window.ws.send('query'));
await expect.poll(() => log).toEqual(['query']);
expect(await page.evaluate(() => window.log)).toEqual(['response']);
});

View file

@ -1390,6 +1390,44 @@ test('should show baseURL in metadata pane', {
await expect(traceViewer.metadataTab).toContainText('baseURL:https://example.com'); await expect(traceViewer.metadataTab).toContainText('baseURL:https://example.com');
}); });
test('should not leak recorders', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33086' },
}, async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]);
const aliveCount = async () => {
return await traceViewer.page.evaluate(() => {
const weakSet = (window as any)._weakRecordersForTest || new Set();
const weakList = [...weakSet];
const aliveList = weakList.filter(r => !!r.deref());
return aliveList.length;
});
};
await expect(traceViewer.snapshotContainer.contentFrame().locator('body')).toContainText(`Hi, I'm frame`);
const frame1 = await traceViewer.snapshotFrame('page.goto');
await expect(frame1.locator('body')).toContainText('Hello world');
const frame2 = await traceViewer.snapshotFrame('page.evaluate');
await expect(frame2.locator('button')).toBeVisible();
await traceViewer.page.requestGC();
await expect.poll(() => aliveCount()).toBeLessThanOrEqual(2); // two snapshot iframes
const frame3 = await traceViewer.snapshotFrame('page.setViewportSize');
await expect(frame3.locator('body')).toContainText(`Hi, I'm frame`);
const frame4 = await traceViewer.snapshotFrame('page.goto');
await expect(frame4.locator('body')).toContainText('Hello world');
const frame5 = await traceViewer.snapshotFrame('page.evaluate');
await expect(frame5.locator('button')).toBeVisible();
await traceViewer.page.requestGC();
await expect.poll(() => aliveCount()).toBeLessThanOrEqual(2); // two snapshot iframes
});
test('should serve css without content-type', async ({ page, runAndTrace, server }) => { test('should serve css without content-type', async ({ page, runAndTrace, server }) => {
server.setRoute('/one-style.css', (req, res) => { server.setRoute('/one-style.css', (req, res) => {
res.writeHead(200); res.writeHead(200);

View file

@ -123,9 +123,10 @@ it('should intercept network activity from worker', async function({ page, serve
it('should intercept worker requests when enabled after worker creation', { it('should intercept worker requests when enabled after worker creation', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32355' } annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32355' }
}, async ({ page, server, isAndroid, browserName }) => { }, async ({ page, server, isAndroid, browserName, browserMajorVersion }) => {
it.skip(isAndroid); it.skip(isAndroid);
it.fixme(browserName === 'chromium'); it.skip(browserName === 'chromium' && browserMajorVersion < 130, 'fixed in Chromium 130');
it.fixme(browserName === 'chromium', 'requires PlzDedicatedWorker to be enabled');
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
server.setRoute('/data_for_worker', (req, res) => res.end('failed to intercept')); server.setRoute('/data_for_worker', (req, res) => res.end('failed to intercept'));

View file

@ -167,7 +167,6 @@ it('should report network activity', async function({ page, server, browserName,
it('should report network activity on worker creation', async function({ page, server, browserName, browserMajorVersion }) { it('should report network activity on worker creation', async function({ page, server, browserName, browserMajorVersion }) {
it.skip(browserName === 'firefox' && browserMajorVersion < 114, 'https://github.com/microsoft/playwright/issues/21760'); it.skip(browserName === 'firefox' && browserMajorVersion < 114, 'https://github.com/microsoft/playwright/issues/21760');
// Chromium needs waitForDebugger enabled for this one.
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const url = server.PREFIX + '/one-style.css'; const url = server.PREFIX + '/one-style.css';
const requestPromise = page.waitForRequest(url); const requestPromise = page.waitForRequest(url);
@ -182,6 +181,19 @@ it('should report network activity on worker creation', async function({ page, s
expect(response.ok()).toBe(true); expect(response.ok()).toBe(true);
}); });
it('should report worker script as network request', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33107' },
}, async function({ page, server }) {
await page.goto(server.EMPTY_PAGE);
const [request1, request2] = await Promise.all([
page.waitForEvent('request', r => r.url().includes('worker.js')),
page.waitForEvent('requestfinished', r => r.url().includes('worker.js')),
page.evaluate(() => (window as any).w = new Worker('/worker/worker.js')),
]);
expect.soft(request1.url()).toBe(server.PREFIX + '/worker/worker.js');
expect.soft(request1).toBe(request2);
});
it('should dispatch console messages when page has workers', async function({ page, server }) { it('should dispatch console messages when page has workers', async function({ page, server }) {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/15550' }); it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/15550' });
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);

View file

@ -4,7 +4,7 @@ set -x
trap "cd $(pwd -P)" EXIT trap "cd $(pwd -P)" EXIT
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)" SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
NODE_VERSION="20.17.0" # autogenerated via ./update-playwright-driver-version.mjs NODE_VERSION="20.18.0" # autogenerated via ./update-playwright-driver-version.mjs
cd "$(dirname "$0")" cd "$(dirname "$0")"
PACKAGE_VERSION=$(node -p "require('../../package.json').version") PACKAGE_VERSION=$(node -p "require('../../package.json').version")

View file

@ -2,7 +2,7 @@ FROM ubuntu:noble
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG TZ=America/Los_Angeles ARG TZ=America/Los_Angeles
ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-jammy" ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-noble"
ENV LANG=C.UTF-8 ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8 ENV LC_ALL=C.UTF-8

View file

@ -16,7 +16,7 @@
// @ts-check // @ts-check
const Documentation = require('./documentation'); const Documentation = require('./documentation');
const { visitAll } = require('../markdown'); const { visitAll, render } = require('../markdown');
/** /**
* @param {Documentation.MarkdownNode[]} nodes * @param {Documentation.MarkdownNode[]} nodes
* @param {number} maxColumns * @param {number} maxColumns
@ -64,7 +64,10 @@ function _innerRenderNodes(nodes, maxColumns = 80, wrapParagraphs = true) {
} else if (node.type === 'li') { } else if (node.type === 'li') {
_wrapInNode('item><description', _wrapAndEscape(node, maxColumns), summary, '/description></item'); _wrapInNode('item><description', _wrapAndEscape(node, maxColumns), summary, '/description></item');
} else if (node.type === 'note') { } else if (node.type === 'note') {
_wrapInNode('para', _wrapAndEscape(node, maxColumns), remarks); _wrapInNode('para', _wrapAndEscape({
type: 'text',
text: render(node.children ?? []).replaceAll('\n', '↵'),
}, maxColumns), remarks);
} }
lastNode = node; lastNode = node;
}); });
@ -75,11 +78,11 @@ function _innerRenderNodes(nodes, maxColumns = 80, wrapParagraphs = true) {
function _wrapCode(lines) { function _wrapCode(lines) {
let i = 0; let i = 0;
let out = []; const out = [];
for (let line of lines) { for (let line of lines) {
line = line.replace(/[&]/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); line = line.replace(/[&]/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
if (i < lines.length - 1) if (i < lines.length - 1)
line = line + "<br/>"; line = line + '<br/>';
out.push(line); out.push(line);
i++; i++;
} }
@ -163,4 +166,4 @@ function renderTextOnly(nodes, maxColumns = 80) {
return result.summary; return result.summary;
} }
module.exports = { renderXmlDoc, renderTextOnly } module.exports = { renderXmlDoc, renderTextOnly };

View file

@ -520,7 +520,8 @@ function renderMethod(member, parent, name, options, out) {
&& !name.startsWith('Get') && !name.startsWith('Get')
&& name !== 'CreateFormData' && name !== 'CreateFormData'
&& !name.startsWith('PostDataJSON') && !name.startsWith('PostDataJSON')
&& !name.startsWith('As')) { && !name.startsWith('As')
&& name !== 'ConnectToServer') {
if (!member.async) { if (!member.async) {
if (member.spec && !options.nodocs) if (member.spec && !options.nodocs)
out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth)); out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
@ -718,7 +719,7 @@ function translateType(type, parent, generateNameCallback = t => t.name, optiona
if (type.expression === '[null]|[Error]') if (type.expression === '[null]|[Error]')
return 'void'; return 'void';
if (type.name == 'Promise' && type.templates?.[0].name === 'any') if (type.name === 'Promise' && type.templates?.[0].name === 'any')
return 'Task'; return 'Task';
if (type.union) { if (type.union) {