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
[![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)
@ -8,9 +8,9 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | 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: |
| 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.

View file

@ -3699,8 +3699,8 @@ await page.routeWebSocket('/ws', ws => {
```java
page.routeWebSocket("/ws", ws -> {
ws.onMessage(message -> {
if ("request".equals(message))
ws.onMessage(frame -> {
if ("request".equals(frame.text()))
ws.send("response");
});
});
@ -3730,8 +3730,8 @@ page.route_web_socket("/ws", handler)
```csharp
await page.RouteWebSocketAsync("/ws", ws => {
ws.OnMessage(message => {
if (message == "request")
ws.OnMessage(frame => {
if (frame.Text == "request")
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"`.
```js
await page.routeWebSocket('/ws', ws => {
await page.routeWebSocket('wss://example.com/ws', ws => {
ws.onMessage(message => {
if (message === 'request')
ws.send('response');
@ -17,9 +17,9 @@ await page.routeWebSocket('/ws', ws => {
```
```java
page.routeWebSocket("/ws", ws -> {
ws.onMessage(message -> {
if ("request".equals(message))
page.routeWebSocket("wss://example.com/ws", ws -> {
ws.onMessage(frame -> {
if ("request".equals(frame.text()))
ws.send("response");
});
});
@ -30,7 +30,7 @@ def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
if message == "request":
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)
))
```
@ -40,15 +40,15 @@ def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
if message == "request":
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)
))
```
```csharp
await page.RouteWebSocketAsync("/ws", ws => {
ws.OnMessage(message => {
if (message == "request")
await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
ws.OnMessage(frame => {
if (frame.Text == "request")
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.
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**
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
page.routeWebSocket("/ws", ws -> {
WebSocketRoute server = ws.connectToServer();
ws.onMessage(message -> {
if ("request".equals(message))
ws.onMessage(frame -> {
if ("request".equals(frame.text()))
server.send("request2");
else
server.send(message);
server.send(frame.text());
});
});
```
@ -117,11 +180,11 @@ page.route_web_socket("/ws", handler)
```csharp
await page.RouteWebSocketAsync("/ws", ws => {
var server = ws.ConnectToServer();
ws.OnMessage(message => {
if (message == "request")
ws.OnMessage(frame => {
if (frame.Text == "request")
server.Send("request2");
else
server.Send(message);
server.Send(frame.Text);
});
});
```
@ -152,13 +215,13 @@ await page.routeWebSocket('/ws', ws => {
```java
page.routeWebSocket("/ws", ws -> {
WebSocketRoute server = ws.connectToServer();
ws.onMessage(message -> {
if (!"blocked-from-the-page".equals(message))
server.send(message);
ws.onMessage(frame -> {
if (!"blocked-from-the-page".equals(frame.text()))
server.send(frame.text());
});
server.onMessage(message -> {
if (!"blocked-from-the-server".equals(message))
ws.send(message);
server.onMessage(frame -> {
if (!"blocked-from-the-server".equals(frame.text()))
ws.send(frame.text());
});
});
```
@ -200,13 +263,13 @@ page.route_web_socket("/ws", handler)
```csharp
await page.RouteWebSocketAsync("/ws", ws => {
var server = ws.ConnectToServer();
ws.OnMessage(message => {
if (message != "blocked-from-the-page")
server.Send(message);
ws.OnMessage(frame => {
if (frame.Text != "blocked-from-the-page")
server.Send(frame.Text);
});
server.OnMessage(message => {
if (message != "blocked-from-the-server")
ws.Send(message);
server.OnMessage(frame => {
if (frame.Text != "blocked-from-the-server")
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
* 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).
### 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
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
* 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.

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).
## 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

@ -10,7 +10,7 @@ Playwright provides APIs to **monitor** and **modify** browser network traffic,
## Mock APIs
Check out our [API mocking guide](./mock.md) to learn more on how to
Check out our [API mocking guide](./mock.md) to learn more on how to
- mock API requests and never hit the API
- perform the API request and modify the response
- use HAR files to mock network requests.
@ -723,7 +723,9 @@ Important notes:
## 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
page.on('websocket', ws => {

View file

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

View file

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

View file

@ -8,6 +8,11 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
## Version 1.48
<LiteYouTube
id="VGlkSBkMVCQ"
title="Playwright 1.48"
/>
### 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"`.

68
package-lock.json generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -554,6 +554,7 @@ async function open(options: Options, url: string | undefined, language: string)
contextOptions,
device: options.device,
saveStorage: options.saveStorage,
handleSIGINT: false,
});
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',
testIdAttributeName,
outputFile: outputFile ? path.resolve(outputFile) : undefined,
handleSIGINT: false,
});
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) {
super(parent, type, guid, initializer);
this.markAsInternalType();
this._server = {
onMessage: (handler: (message: string | Buffer) => any) => {

View file

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

View file

@ -37,7 +37,8 @@ export const chromiumSwitches = [
// PaintHolding - https://github.com/microsoft/playwright/issues/28023
// ThirdPartyStoragePartitioning - https://github.com/microsoft/playwright/issues/32230
// 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',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',

View file

@ -110,7 +110,7 @@
"defaultBrowserType": "webkit"
},
"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": {
"width": 360,
"height": 640
@ -121,7 +121,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 640,
"height": 360
@ -132,7 +132,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 360,
"height": 740
@ -143,7 +143,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 740,
"height": 360
@ -154,7 +154,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 320,
"height": 658
@ -165,7 +165,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 658,
"height": 320
@ -176,7 +176,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 712,
"height": 1138
@ -187,7 +187,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 1138,
"height": 712
@ -1098,7 +1098,7 @@
"defaultBrowserType": "webkit"
},
"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": {
"width": 384,
"height": 640
@ -1109,7 +1109,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 640,
"height": 384
@ -1120,7 +1120,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 640,
"height": 360
@ -1131,7 +1131,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 360,
"height": 640
@ -1142,7 +1142,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 360,
"height": 640
@ -1153,7 +1153,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 640,
"height": 360
@ -1164,7 +1164,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 800,
"height": 1280
@ -1175,7 +1175,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 1280,
"height": 800
@ -1186,7 +1186,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 384,
"height": 640
@ -1197,7 +1197,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 640,
"height": 384
@ -1208,7 +1208,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 360,
"height": 640
@ -1219,7 +1219,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 640,
"height": 360
@ -1230,7 +1230,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 412,
"height": 732
@ -1241,7 +1241,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 732,
"height": 412
@ -1252,7 +1252,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 412,
"height": 732
@ -1263,7 +1263,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 732,
"height": 412
@ -1274,7 +1274,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 412,
"height": 732
@ -1285,7 +1285,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 732,
"height": 412
@ -1296,7 +1296,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 600,
"height": 960
@ -1307,7 +1307,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 960,
"height": 600
@ -1362,7 +1362,7 @@
"defaultBrowserType": "webkit"
},
"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": {
"width": 411,
"height": 731
@ -1373,7 +1373,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 731,
"height": 411
@ -1384,7 +1384,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 411,
"height": 823
@ -1395,7 +1395,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 823,
"height": 411
@ -1406,7 +1406,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 393,
"height": 786
@ -1417,7 +1417,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 786,
"height": 393
@ -1428,7 +1428,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 353,
"height": 745
@ -1439,7 +1439,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 745,
"height": 353
@ -1450,7 +1450,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 412,
"height": 892
@ -1465,7 +1465,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"height": 892,
"width": 412
@ -1480,7 +1480,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 393,
"height": 851
@ -1495,7 +1495,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 851,
"height": 393
@ -1510,7 +1510,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 412,
"height": 915
@ -1525,7 +1525,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 915,
"height": 412
@ -1540,7 +1540,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 360,
"height": 640
@ -1551,7 +1551,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 640,
"height": 360
@ -1562,7 +1562,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 1792,
"height": 1120
@ -1577,7 +1577,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 1792,
"height": 1120
@ -1592,7 +1592,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 1792,
"height": 1120
@ -1622,7 +1622,7 @@
"defaultBrowserType": "webkit"
},
"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": {
"width": 1920,
"height": 1080
@ -1637,7 +1637,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 1920,
"height": 1080
@ -1652,7 +1652,7 @@
"defaultBrowserType": "chromium"
},
"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": {
"width": 1920,
"height": 1080

View file

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

View file

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

View file

@ -1051,7 +1051,7 @@ export class Recorder {
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.document.adoptedStyleSheets.push(this._stylesheet);

View file

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

View file

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

View file

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

View file

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

View file

@ -21,6 +21,7 @@ import type { EventEmitter } from 'events';
export interface IRecorder {
setMode(mode: Mode): void;
mode(): Mode;
readonly handleSIGINT: boolean | undefined;
}
export interface IRecorderApp extends EventEmitter {
@ -28,7 +29,7 @@ export interface IRecorderApp extends EventEmitter {
close(): Promise<void>;
setPaused(paused: boolean): 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>;
updateCallLogs(callLogs: CallLog[]): 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 });
}
async setFile(file: string): Promise<void> {
this._transport.deliverEvent('setFileIfNeeded', { file });
async setRunningFile(file: string | undefined): Promise<void> {
this._transport.deliverEvent('setRunningFile', { file });
}
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 = [
'https://playwright.azureedge.net',
'https://playwright-akamai.azureedge.net',
'https://playwright-verizon.azureedge.net',
];
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"`.
*
* ```js
* await page.routeWebSocket('/ws', ws => {
* await page.routeWebSocket('wss://example.com/ws', ws => {
* ws.onMessage(message => {
* if (message === 'request')
* 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 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**
*
* 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",
"version": "1.48.0-next",
"version": "1.48.2",
"description": "Playwright Component Testing Helpers",
"repository": {
"type": "git",
@ -26,8 +26,8 @@
}
},
"dependencies": {
"playwright-core": "1.48.0-next",
"playwright-core": "1.48.2",
"vite": "^5.2.8",
"playwright": "1.48.0-next"
"playwright": "1.48.2"
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -41,13 +41,11 @@ export const Recorder: React.FC<RecorderProps> = ({
log,
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');
React.useEffect(() => {
if (!fileId && sources.length > 0)
setFileId(sources[0].id);
}, [fileId, sources]);
const fileId = selectedFileId || runningFileId || sources[0]?.id;
const source = React.useMemo(() => {
if (fileId) {
@ -66,7 +64,7 @@ export const Recorder: React.FC<RecorderProps> = ({
setLocator(asLocator(language, selector));
};
window.playwrightSetFile = setFileId;
window.playwrightSetRunningFile = setRunningFileId;
const messagesEndRef = React.useRef<HTMLDivElement>(null);
React.useLayoutEffect(() => {
@ -134,19 +132,19 @@ export const Recorder: React.FC<RecorderProps> = ({
<ToolbarButton icon='files' title='Copy' disabled={!source || !source.text} onClick={() => {
copy(source.text);
}}></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' });
}}></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' });
}}></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' });
}}></ToolbarButton>
<div style={{ flex: 'auto' }}></div>
<div>Target:</div>
<SourceChooser fileId={fileId} sources={sources} setFileId={fileId => {
setFileId(fileId);
setSelectedFileId(fileId);
window.dispatch({ event: 'fileChanged', params: { file: fileId } });
}} />
<ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => {

View file

@ -96,7 +96,7 @@ declare global {
playwrightSetSources: (sources: Source[]) => void;
playwrightSetOverlayVisible: (visible: boolean) => void;
playwrightUpdateLogs: (callLogs: CallLog[]) => void;
playwrightSetFile: (file: string) => void;
playwrightSetRunningFile: (file: string | undefined) => void;
playwrightSetSelector: (selector: string, focus?: boolean) => void;
playwrightSourcesEchoForTest: Source[];
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 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();
let set = clientIdToTraceUrls.get(clientId);
if (!set) {
set = new Set();
clientIdToTraceUrls.set(clientId, set);
let data = clientIdToTraceUrls.get(clientId);
if (!data) {
data = { limit, traceUrls: new Set() };
clientIdToTraceUrls.set(clientId, data);
}
set.add(traceUrl);
data.traceUrls.add(traceUrl);
const traceModel = new TraceModel();
try {
@ -97,7 +97,8 @@ async function doFetch(event: FetchEvent): Promise<Response> {
if (relativePath === '/contexts') {
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 } });
});
return new Response(JSON.stringify(traceModel!.contextEntries), {
@ -172,12 +173,18 @@ async function gc() {
const clients = await self.clients.matchAll();
const usedTraces = new Set<string>();
for (const [clientId, traceUrls] of clientIdToTraceUrls) {
for (const [clientId, data] of clientIdToTraceUrls) {
// @ts-ignore
if (!clients.find(c => c.id === clientId))
if (!clients.find(c => c.id === clientId)) {
clientIdToTraceUrls.delete(clientId);
else
traceUrls.forEach(url => usedTraces.add(url));
continue;
}
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()) {

View file

@ -16,6 +16,7 @@
import { escapeHTMLAttribute, escapeHTML } from '@isomorphic/stringUtils';
import type { FrameSnapshot, NodeNameAttributesChildNodesSnapshot, NodeSnapshot, RenderedFrameSnapshot, ResourceSnapshot, SubtreeReferenceSnapshot } from '@trace/snapshot';
import type { LRUCache } from './lruCache';
function isNodeNameAttributesChildNodesSnapshot(n: NodeSnapshot): n is NodeNameAttributesChildNodesSnapshot {
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]);
}
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 {
private _htmlCache: LRUCache<SnapshotRenderer, string>;
private _snapshots: FrameSnapshot[];
private _index: number;
readonly snapshotName: string | undefined;
@ -61,7 +35,8 @@ export class SnapshotRenderer {
private _snapshot: FrameSnapshot;
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._snapshots = snapshots;
this._index = index;
@ -151,16 +126,15 @@ export class SnapshotRenderer {
};
const snapshot = this._snapshot;
const html = lruCache(this, () => {
const html = this._htmlCache.getOrCompute(this, () => {
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}>` : '';
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>',
`<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 };

View file

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

View file

@ -20,7 +20,7 @@ import { ImageDiffView } from '@web/shared/imageDiffView';
import type { MultiTraceModel } from './modelUtil';
import { PlaceholderPanel } from './placeholderPanel';
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 { Expandable } from '@web/components/expandable';
import { linkifyText } from '@web/renderUtils';
@ -51,6 +51,11 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
}
}, [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 }}>
{linkifyText(attachment.name)} {hasContent && <a style={{ marginLeft: 5 }} href={downloadURL(attachment)}>download</a>}
</span>;
@ -62,14 +67,16 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
<Expandable title={title} expanded={expanded} setExpanded={setExpanded} expandOnTitleClick={true}>
{placeholder && <i>{placeholder}</i>}
</Expandable>
{expanded && attachmentText !== null && <CodeMirrorWrapper
text={attachmentText}
readOnly
mimeType={attachment.contentType}
linkify={true}
lineNumbers={true}
wrapLines={false}>
</CodeMirrorWrapper>}
{expanded && attachmentText !== null && <div className='vbox' style={{ height: snippetHeight }}>
<CodeMirrorWrapper
text={attachmentText}
readOnly
mimeType={attachment.contentType}
linkify={true}
lineNumbers={true}
wrapLines={false}>
</CodeMirrorWrapper>
</div>}
</>;
};

View file

@ -65,6 +65,7 @@ export const EmbeddedWorkbenchLoader: React.FunctionComponent = () => {
const url = traceURLs[i];
const params = new URLSearchParams();
params.set('trace', url);
params.set('limit', String(traceURLs.length));
const response = await fetch(`contexts?${params.toString()}`);
if (!response.ok) {
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 }> {
const params = new URLSearchParams();
params.set('trace', url);
params.set('limit', '1');
const response = await fetch(`contexts?${params.toString()}`);
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 { Toolbar } from '@web/components/toolbar';
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 { Recorder } from '@injected/recorder/recorder';
import ConsoleAPI from '@injected/consoleApi';
@ -52,7 +52,7 @@ export const SnapshotTabsView: React.FunctionComponent<{
openPage?: (url: string, target?: string) => Window | any,
}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator, openPage }) => {
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
const [showScreenshotInsteadOfSnapshot] = useSetting('screenshot-instead-of-snapshot', false);
const showScreenshotInsteadOfSnapshot = false;
const snapshots = React.useMemo(() => {
return collectSnapshots(action);
@ -315,6 +315,10 @@ function createRecorders(recorders: { recorder: Recorder, frameSelector: string
const recorder = new Recorder(injectedScript);
win._injectedScript = injectedScript;
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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,8 +20,6 @@ import type { AddressInfo } from 'net';
const CDNS = [
'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;

View file

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

View file

@ -171,6 +171,13 @@ export class Recorder {
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 }> {
let callback;
const listener = async msg => {
@ -185,8 +192,8 @@ export class Recorder {
return new Promise(f => callback = f);
}
async hoverOverElement(selector: string, options?: { position?: { x: number, y: number }}): Promise<string> {
return this.waitForHighlight(async () => {
async hoverOverElement(selector: string, options?: { position?: { x: number, y: number }, omitTooltip?: boolean }): Promise<string> {
return (options?.omitTooltip ? this.waitForHighlightNoTooltip : this.waitForHighlight).call(this, async () => {
const box = await this.page.locator(selector).first().boundingBox();
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);

View file

@ -15,7 +15,7 @@
*/
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';
@ -103,6 +103,7 @@ it.describe('pause', () => {
await page.pause();
})();
const recorderPage = await recorderPageGetter();
await expect(recorderPage.getByRole('combobox', { name: 'Source chooser' })).toHaveValue(/pause\.spec\.ts/);
const source = await recorderPage.textContent('.source-line-paused');
expect(source).toContain('page.pause()');
await recorderPage.click('[title="Resume (F8)"]');
@ -480,6 +481,26 @@ it.describe('pause', () => {
await recorderPage.click('[title="Resume (F8)"]');
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[]> {

View file

@ -508,3 +508,27 @@ test('should throw when connecting twice', async ({ page, server }) => {
const error = await promise;
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');
});
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 }) => {
server.setRoute('/one-style.css', (req, res) => {
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', {
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.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);
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.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);
const url = server.PREFIX + '/one-style.css';
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);
});
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.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/15550' });
await page.goto(server.EMPTY_PAGE);

View file

@ -4,7 +4,7 @@ set -x
trap "cd $(pwd -P)" EXIT
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")"
PACKAGE_VERSION=$(node -p "require('../../package.json').version")

View file

@ -2,7 +2,7 @@ FROM ubuntu:noble
ARG DEBIAN_FRONTEND=noninteractive
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 LC_ALL=C.UTF-8

View file

@ -16,7 +16,7 @@
// @ts-check
const Documentation = require('./documentation');
const { visitAll } = require('../markdown');
const { visitAll, render } = require('../markdown');
/**
* @param {Documentation.MarkdownNode[]} nodes
* @param {number} maxColumns
@ -64,7 +64,10 @@ function _innerRenderNodes(nodes, maxColumns = 80, wrapParagraphs = true) {
} else if (node.type === 'li') {
_wrapInNode('item><description', _wrapAndEscape(node, maxColumns), summary, '/description></item');
} 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;
});
@ -75,11 +78,11 @@ function _innerRenderNodes(nodes, maxColumns = 80, wrapParagraphs = true) {
function _wrapCode(lines) {
let i = 0;
let out = [];
const out = [];
for (let line of lines) {
line = line.replace(/[&]/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
if (i < lines.length - 1)
line = line + "<br/>";
line = line + '<br/>';
out.push(line);
i++;
}
@ -163,4 +166,4 @@ function renderTextOnly(nodes, maxColumns = 80) {
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 !== 'CreateFormData'
&& !name.startsWith('PostDataJSON')
&& !name.startsWith('As')) {
&& !name.startsWith('As')
&& name !== 'ConnectToServer') {
if (!member.async) {
if (member.spec && !options.nodocs)
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]')
return 'void';
if (type.name == 'Promise' && type.templates?.[0].name === 'any')
if (type.name === 'Promise' && type.templates?.[0].name === 'any')
return 'Task';
if (type.union) {