Merge branch 'main' into sharding-algorithm
This commit is contained in:
commit
ab3ab83955
6
.github/workflows/tests_bidi.yml
vendored
6
.github/workflows/tests_bidi.yml
vendored
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
channel: [bidi-chromium, bidi-firefox-beta]
|
||||
channel: [bidi-chromium, bidi-firefox-nightly]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
|
|
@ -38,8 +38,8 @@ jobs:
|
|||
- run: npm run build
|
||||
- run: npx playwright install --with-deps chromium
|
||||
if: matrix.channel == 'bidi-chromium'
|
||||
- run: npx -y @puppeteer/browsers install firefox@beta
|
||||
if: matrix.channel == 'bidi-firefox-beta'
|
||||
- run: npx -y @puppeteer/browsers install firefox@nightly
|
||||
if: matrix.channel == 'bidi-firefox-nightly'
|
||||
- name: Run tests
|
||||
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run biditest -- --project=${{ matrix.channel }}*
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 🎭 Playwright
|
||||
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
|
||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|||
|
||||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->129.0.6668.42<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Chromium <!-- GEN:chromium-version -->130.0.6723.6<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->130.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
|
|
|
|||
|
|
@ -1266,6 +1266,99 @@ When set to `minimal`, only record information necessary for routing from HAR. T
|
|||
|
||||
Optional setting to control resource content management. If `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is specified, content is stored inline the HAR file.
|
||||
|
||||
|
||||
## async method: BrowserContext.routeWebSocket
|
||||
* since: v1.48
|
||||
|
||||
This method allows to modify websocket connections that are made by any page in the browser context.
|
||||
|
||||
Note that only `WebSocket`s created after this method was called will be routed. It is recommended to call this method before creating any pages.
|
||||
|
||||
**Usage**
|
||||
|
||||
Below is an example of a simple handler that blocks some websocket messages.
|
||||
See [WebSocketRoute] for more details and examples.
|
||||
|
||||
```js
|
||||
await context.routeWebSocket('/ws', async ws => {
|
||||
ws.routeSend(message => {
|
||||
if (message === 'to-be-blocked')
|
||||
return;
|
||||
ws.send(message);
|
||||
});
|
||||
await ws.connect();
|
||||
});
|
||||
```
|
||||
|
||||
```java
|
||||
context.routeWebSocket("/ws", ws -> {
|
||||
ws.routeSend(message -> {
|
||||
if ("to-be-blocked".equals(message))
|
||||
return;
|
||||
ws.send(message);
|
||||
});
|
||||
ws.connect();
|
||||
});
|
||||
```
|
||||
|
||||
```python async
|
||||
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
|
||||
if message == "to-be-blocked":
|
||||
return
|
||||
ws.send(message)
|
||||
|
||||
async def handler(ws: WebSocketRoute):
|
||||
ws.route_send(lambda message: message_handler(ws, message))
|
||||
await ws.connect()
|
||||
|
||||
await context.route_web_socket("/ws", handler)
|
||||
```
|
||||
|
||||
```python sync
|
||||
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
|
||||
if message == "to-be-blocked":
|
||||
return
|
||||
ws.send(message)
|
||||
|
||||
def handler(ws: WebSocketRoute):
|
||||
ws.route_send(lambda message: message_handler(ws, message))
|
||||
ws.connect()
|
||||
|
||||
context.route_web_socket("/ws", handler)
|
||||
```
|
||||
|
||||
```csharp
|
||||
await context.RouteWebSocketAsync("/ws", async ws => {
|
||||
ws.RouteSend(message => {
|
||||
if (message == "to-be-blocked")
|
||||
return;
|
||||
ws.Send(message);
|
||||
});
|
||||
await ws.ConnectAsync();
|
||||
});
|
||||
```
|
||||
|
||||
### param: BrowserContext.routeWebSocket.url
|
||||
* since: v1.48
|
||||
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
|
||||
|
||||
Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the [`option: baseURL`] from the context options.
|
||||
|
||||
### param: BrowserContext.routeWebSocket.handler
|
||||
* since: v1.48
|
||||
* langs: js, python
|
||||
- `handler` <[function]\([WebSocketRoute]\): [Promise<any>|any]>
|
||||
|
||||
Handler function to route the WebSocket.
|
||||
|
||||
### param: BrowserContext.routeWebSocket.handler
|
||||
* since: v1.48
|
||||
* langs: csharp, java
|
||||
- `handler` <[function]\([WebSocketRoute]\)>
|
||||
|
||||
Handler function to route the WebSocket.
|
||||
|
||||
|
||||
## method: BrowserContext.serviceWorkers
|
||||
* since: v1.11
|
||||
* langs: js, python
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
# class: FrameLocator
|
||||
* since: v1.17
|
||||
|
||||
FrameLocator represents a view to the `iframe` on the page. It captures the logic sufficient to retrieve the `iframe` and locate elements in that iframe. FrameLocator can be created with either [`method: Page.frameLocator`] or [`method: Locator.frameLocator`] method.
|
||||
FrameLocator represents a view to the `iframe` on the page. It captures the logic sufficient to retrieve the `iframe` and locate elements in that iframe. FrameLocator can be created with either [`method: Locator.contentFrame`], [`method: Page.frameLocator`] or [`method: Locator.frameLocator`] method.
|
||||
|
||||
```js
|
||||
const locator = page.frameLocator('#my-frame').getByText('Submit');
|
||||
const locator = page.locator('#my-frame').contentFrame().getByText('Submit');
|
||||
await locator.click();
|
||||
```
|
||||
|
||||
```java
|
||||
Locator locator = page.frameLocator("#my-frame").getByText("Submit");
|
||||
Locator locator = page.locator("#my-frame").contentFrame().getByText("Submit");
|
||||
locator.click();
|
||||
```
|
||||
|
||||
```python async
|
||||
locator = page.frame_locator("#my-frame").get_by_text("Submit")
|
||||
locator = page.locator("#my-frame").content_frame.get_by_text("Submit")
|
||||
await locator.click()
|
||||
```
|
||||
|
||||
```python sync
|
||||
locator = page.frame_locator("my-frame").get_by_text("Submit")
|
||||
locator = page.locator("my-frame").content_frame.get_by_text("Submit")
|
||||
locator.click()
|
||||
```
|
||||
|
||||
```csharp
|
||||
var locator = page.FrameLocator("#my-frame").GetByText("Submit");
|
||||
var locator = page.Locator("#my-frame").ContentFrame.GetByText("Submit");
|
||||
await locator.ClickAsync();
|
||||
```
|
||||
|
||||
|
|
@ -34,42 +34,42 @@ Frame locators are strict. This means that all operations on frame locators will
|
|||
|
||||
```js
|
||||
// Throws if there are several frames in DOM:
|
||||
await page.frameLocator('.result-frame').getByRole('button').click();
|
||||
await page.locator('.result-frame').contentFrame().getByRole('button').click();
|
||||
|
||||
// Works because we explicitly tell locator to pick the first frame:
|
||||
await page.frameLocator('.result-frame').first().getByRole('button').click();
|
||||
await page.locator('.result-frame').contentFrame().first().getByRole('button').click();
|
||||
```
|
||||
|
||||
```python async
|
||||
# Throws if there are several frames in DOM:
|
||||
await page.frame_locator('.result-frame').get_by_role('button').click()
|
||||
await page.locator('.result-frame').content_frame.get_by_role('button').click()
|
||||
|
||||
# Works because we explicitly tell locator to pick the first frame:
|
||||
await page.frame_locator('.result-frame').first.get_by_role('button').click()
|
||||
await page.locator('.result-frame').first.content_frame.get_by_role('button').click()
|
||||
```
|
||||
|
||||
```python sync
|
||||
# Throws if there are several frames in DOM:
|
||||
page.frame_locator('.result-frame').get_by_role('button').click()
|
||||
page.locator('.result-frame').content_frame.get_by_role('button').click()
|
||||
|
||||
# Works because we explicitly tell locator to pick the first frame:
|
||||
page.frame_locator('.result-frame').first.get_by_role('button').click()
|
||||
page.locator('.result-frame').first.content_frame.get_by_role('button').click()
|
||||
```
|
||||
|
||||
```java
|
||||
// Throws if there are several frames in DOM:
|
||||
page.frame_locator(".result-frame").getByRole(AriaRole.BUTTON).click();
|
||||
page.locator(".result-frame").contentFrame().getByRole(AriaRole.BUTTON).click();
|
||||
|
||||
// Works because we explicitly tell locator to pick the first frame:
|
||||
page.frame_locator(".result-frame").first().getByRole(AriaRole.BUTTON).click();
|
||||
page.locator(".result-frame").first().contentFrame().getByRole(AriaRole.BUTTON).click();
|
||||
```
|
||||
|
||||
```csharp
|
||||
// Throws if there are several frames in DOM:
|
||||
await page.FrameLocator(".result-frame").GetByRole(AriaRole.Button).ClickAsync();
|
||||
await page.Locator(".result-frame").ContentFrame.GetByRole(AriaRole.Button).ClickAsync();
|
||||
|
||||
// Works because we explicitly tell locator to pick the first frame:
|
||||
await page.FrameLocator(".result-frame").First.getByRole(AriaRole.Button).ClickAsync();
|
||||
await page.Locator(".result-frame").First.ContentFrame.getByRole(AriaRole.Button).ClickAsync();
|
||||
```
|
||||
|
||||
**Converting Locator to FrameLocator**
|
||||
|
|
@ -82,6 +82,7 @@ If you have a [FrameLocator] object it can be converted to [Locator] pointing to
|
|||
|
||||
|
||||
## method: FrameLocator.first
|
||||
* deprecated: Use [`method: Locator.first`] followed by [`method: Locator.contentFrame`] instead.
|
||||
* since: v1.17
|
||||
- returns: <[FrameLocator]>
|
||||
|
||||
|
|
@ -171,6 +172,7 @@ in that iframe.
|
|||
### option: FrameLocator.getByTitle.exact = %%-locator-get-by-text-exact-%%
|
||||
|
||||
## method: FrameLocator.last
|
||||
* deprecated: Use [`method: Locator.last`] followed by [`method: Locator.contentFrame`] instead.
|
||||
* since: v1.17
|
||||
- returns: <[FrameLocator]>
|
||||
|
||||
|
|
@ -195,6 +197,7 @@ Returns locator to the last matching frame.
|
|||
* since: v1.33
|
||||
|
||||
## method: FrameLocator.nth
|
||||
* deprecated: Use [`method: Locator.nth`] followed by [`method: Locator.contentFrame`] instead.
|
||||
* since: v1.17
|
||||
- returns: <[FrameLocator]>
|
||||
|
||||
|
|
@ -217,37 +220,36 @@ For a reverse operation, use [`method: Locator.contentFrame`].
|
|||
**Usage**
|
||||
|
||||
```js
|
||||
const frameLocator = page.frameLocator('iframe[name="embedded"]');
|
||||
const frameLocator = page.locator('iframe[name="embedded"]').contentFrame();
|
||||
// ...
|
||||
const locator = frameLocator.owner();
|
||||
await expect(locator).toBeVisible();
|
||||
```
|
||||
|
||||
```java
|
||||
FrameLocator frameLocator = page.frameLocator("iframe[name=\"embedded\"]");
|
||||
FrameLocator frameLocator = page.locator("iframe[name=\"embedded\"]").contentFrame();
|
||||
// ...
|
||||
Locator locator = frameLocator.owner();
|
||||
assertThat(locator).isVisible();
|
||||
```
|
||||
|
||||
```python async
|
||||
frame_locator = page.frame_locator("iframe[name=\"embedded\"]")
|
||||
frame_locator = page.locator("iframe[name=\"embedded\"]").content_frame
|
||||
# ...
|
||||
locator = frame_locator.owner
|
||||
await expect(locator).to_be_visible()
|
||||
```
|
||||
|
||||
```python sync
|
||||
frame_locator = page.frame_locator("iframe[name=\"embedded\"]")
|
||||
frame_locator = page.locator("iframe[name=\"embedded\"]").content_frame
|
||||
# ...
|
||||
locator = frame_locator.owner
|
||||
expect(locator).to_be_visible()
|
||||
```
|
||||
|
||||
```csharp
|
||||
var frameLocator = Page.FrameLocator("iframe[name=\"embedded\"]");
|
||||
var frameLocator = Page.Locator("iframe[name=\"embedded\"]").ContentFrame;
|
||||
// ...
|
||||
var locator = frameLocator.Owner;
|
||||
await Expect(locator).ToBeVisibleAsync();
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -3632,6 +3632,99 @@ When set to `minimal`, only record information necessary for routing from HAR. T
|
|||
|
||||
Optional setting to control resource content management. If `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is specified, content is stored inline the HAR file.
|
||||
|
||||
|
||||
## async method: Page.routeWebSocket
|
||||
* since: v1.48
|
||||
|
||||
This method allows to modify websocket connections that are made by the page.
|
||||
|
||||
Note that only `WebSocket`s created after this method was called will be routed. It is recommended to call this method before navigating the page.
|
||||
|
||||
**Usage**
|
||||
|
||||
Below is an example of a simple handler that blocks some websocket messages.
|
||||
See [WebSocketRoute] for more details and examples.
|
||||
|
||||
```js
|
||||
await page.routeWebSocket('/ws', async ws => {
|
||||
ws.routeSend(message => {
|
||||
if (message === 'to-be-blocked')
|
||||
return;
|
||||
ws.send(message);
|
||||
});
|
||||
await ws.connect();
|
||||
});
|
||||
```
|
||||
|
||||
```java
|
||||
page.routeWebSocket("/ws", ws -> {
|
||||
ws.routeSend(message -> {
|
||||
if ("to-be-blocked".equals(message))
|
||||
return;
|
||||
ws.send(message);
|
||||
});
|
||||
ws.connect();
|
||||
});
|
||||
```
|
||||
|
||||
```python async
|
||||
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
|
||||
if message == "to-be-blocked":
|
||||
return
|
||||
ws.send(message)
|
||||
|
||||
async def handler(ws: WebSocketRoute):
|
||||
ws.route_send(lambda message: message_handler(ws, message))
|
||||
await ws.connect()
|
||||
|
||||
await page.route_web_socket("/ws", handler)
|
||||
```
|
||||
|
||||
```python sync
|
||||
def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
|
||||
if message == "to-be-blocked":
|
||||
return
|
||||
ws.send(message)
|
||||
|
||||
def handler(ws: WebSocketRoute):
|
||||
ws.route_send(lambda message: message_handler(ws, message))
|
||||
ws.connect()
|
||||
|
||||
page.route_web_socket("/ws", handler)
|
||||
```
|
||||
|
||||
```csharp
|
||||
await page.RouteWebSocketAsync("/ws", async ws => {
|
||||
ws.RouteSend(message => {
|
||||
if (message == "to-be-blocked")
|
||||
return;
|
||||
ws.Send(message);
|
||||
});
|
||||
await ws.ConnectAsync();
|
||||
});
|
||||
```
|
||||
|
||||
### param: Page.routeWebSocket.url
|
||||
* since: v1.48
|
||||
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
|
||||
|
||||
Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the [`option: baseURL`] from the context options.
|
||||
|
||||
### param: Page.routeWebSocket.handler
|
||||
* since: v1.48
|
||||
* langs: js, python
|
||||
- `handler` <[function]\([WebSocketRoute]\): [Promise<any>|any]>
|
||||
|
||||
Handler function to route the WebSocket.
|
||||
|
||||
### param: Page.routeWebSocket.handler
|
||||
* since: v1.48
|
||||
* langs: csharp, java
|
||||
- `handler` <[function]\([WebSocketRoute]\)>
|
||||
|
||||
Handler function to route the WebSocket.
|
||||
|
||||
|
||||
## async method: Page.screenshot
|
||||
* since: v1.8
|
||||
- returns: <[Buffer]>
|
||||
|
|
|
|||
166
docs/src/api/class-websocketroute.md
Normal file
166
docs/src/api/class-websocketroute.md
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
# class: WebSocketRoute
|
||||
* since: v1.48
|
||||
|
||||
Whenever a [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) route is set up with [`method: Page.routeWebSocket`] or [`method: BrowserContext.routeWebSocket`], the `WebSocketRoute` object allows to handle the WebSocket.
|
||||
|
||||
By default, the routed WebSocket will not actually connect to the server. This way, you can mock entire communcation over the WebSocket. Here is an example that responds to a `"query"` with a `"result"`.
|
||||
|
||||
```js
|
||||
await page.routeWebSocket('/ws', async ws => {
|
||||
ws.routeSend(message => {
|
||||
if (message === 'query')
|
||||
ws.receive('result');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```java
|
||||
page.routeWebSocket("/ws", ws -> {
|
||||
ws.routeSend(message -> {
|
||||
if ("query".equals(message))
|
||||
ws.receive("result");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```python async
|
||||
def message_handler(ws, message):
|
||||
if message == "query":
|
||||
ws.receive("result")
|
||||
|
||||
await page.route_web_socket("/ws", lambda ws: ws.route_send(
|
||||
lambda message: message_handler(ws, message)
|
||||
))
|
||||
```
|
||||
|
||||
```python sync
|
||||
def message_handler(ws, message):
|
||||
if message == "query":
|
||||
ws.receive("result")
|
||||
|
||||
page.route_web_socket("/ws", lambda ws: ws.route_send(
|
||||
lambda message: message_handler(ws, message)
|
||||
))
|
||||
```
|
||||
|
||||
```csharp
|
||||
await page.RouteWebSocketAsync("/ws", async ws => {
|
||||
ws.RouteSend(message => {
|
||||
if (message == "query")
|
||||
ws.receive("result");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## event: WebSocketRoute.close
|
||||
* since: v1.48
|
||||
|
||||
Emitted when the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) closes.
|
||||
|
||||
|
||||
|
||||
## async method: WebSocketRoute.close
|
||||
* since: v1.48
|
||||
|
||||
Closes the server connection and the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page.
|
||||
|
||||
### option: WebSocketRoute.close.code
|
||||
* since: v1.48
|
||||
- `code` <[int]>
|
||||
|
||||
Optional [close code](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#code).
|
||||
|
||||
### option: WebSocketRoute.close.reason
|
||||
* since: v1.48
|
||||
- `reason` <[string]>
|
||||
|
||||
Optional [close reason](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#reason).
|
||||
|
||||
|
||||
## async method: WebSocketRoute.connect
|
||||
* since: v1.48
|
||||
|
||||
By default, routed WebSocket does not connect to the server, so you can mock entire WebSocket communication. This method connects to the actual WebSocket server, giving the ability to send and receive messages from the server.
|
||||
|
||||
Once connected:
|
||||
* Messages received from the server will be automatically dispatched to the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page, unless [`method: WebSocketRoute.routeReceive`] is called.
|
||||
* Messages sent by the `WebSocket.send()` call in the page will be automatically sent to the server, unless [`method: WebSocketRoute.routeSend`] is called.
|
||||
|
||||
|
||||
## method: WebSocketRoute.receive
|
||||
* since: v1.48
|
||||
|
||||
Dispatches a message to the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page, like it was received from the server.
|
||||
|
||||
### param: WebSocketRoute.receive.message
|
||||
* since: v1.48
|
||||
- `message` <[string]|[Buffer]>
|
||||
|
||||
Message to receive.
|
||||
|
||||
|
||||
## async method: WebSocketRoute.routeReceive
|
||||
* since: v1.48
|
||||
|
||||
This method allows to route messages that are received by the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page from the server. This method only makes sense if you are also calling [`method: WebSocketRoute.connect`].
|
||||
|
||||
Once this method is called, received messages are not automatically dispatched to the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page - you should do that manually by calling [`method: WebSocketRoute.receive`].
|
||||
|
||||
Calling this method again times will override the handler with a new one.
|
||||
|
||||
### param: WebSocketRoute.routeReceive.handler
|
||||
* since: v1.48
|
||||
* langs: js, python
|
||||
- `handler` <[function]\([string]\): [Promise<any>|any]>
|
||||
|
||||
Handler function to route received messages.
|
||||
|
||||
### param: WebSocketRoute.routeReceive.handler
|
||||
* since: v1.48
|
||||
* langs: csharp, java
|
||||
- `handler` <[function]\([WebSocketFrame]\)>
|
||||
|
||||
Handler function to route received messages.
|
||||
|
||||
|
||||
|
||||
## async method: WebSocketRoute.routeSend
|
||||
* since: v1.48
|
||||
|
||||
This method allows to route messages that are sent by `WebSocket.send()` call in the page, instead of actually sending them to the server. Once this method is called, sent messages **are not** automatically forwarded to the server - you should do that manually by calling [`method: WebSocketRoute.send`].
|
||||
|
||||
Calling this method again times will override the handler with a new one.
|
||||
|
||||
### param: WebSocketRoute.routeSend.handler
|
||||
* since: v1.48
|
||||
* langs: js, python
|
||||
- `handler` <[function]\([string]|[Buffer]\): [Promise<any>|any]>
|
||||
|
||||
Handler function to route sent messages.
|
||||
|
||||
### param: WebSocketRoute.routeSend.handler
|
||||
* since: v1.48
|
||||
* langs: csharp, java
|
||||
- `handler` <[function]\([WebSocketFrame]\)>
|
||||
|
||||
Handler function to route sent messages.
|
||||
|
||||
|
||||
## method: WebSocketRoute.send
|
||||
* since: v1.48
|
||||
|
||||
Sends a message to the server, like it was sent in the page with `WebSocket.send()`.
|
||||
|
||||
### param: WebSocketRoute.send.message
|
||||
* since: v1.48
|
||||
- `message` <[string]|[Buffer]>
|
||||
|
||||
Message to send.
|
||||
|
||||
|
||||
## method: WebSocketRoute.url
|
||||
* since: v1.48
|
||||
- returns: <[string]>
|
||||
|
||||
URL of the WebSocket created in the page.
|
||||
379
package-lock.json
generated
379
package-lock.json
generated
|
|
@ -40,7 +40,7 @@
|
|||
"@zip.js/zip.js": "^2.7.29",
|
||||
"ansi-styles": "^4.3.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"chromium-bidi": "^0.6.4",
|
||||
"chromium-bidi": "^0.7.1",
|
||||
"colors": "^1.4.0",
|
||||
"concurrently": "^6.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
"react-dom": "^18.1.0",
|
||||
"ssim.js": "^3.5.0",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.0.13",
|
||||
"vite": "^5.4.6",
|
||||
"ws": "^8.17.1",
|
||||
"xml2js": "^0.5.0",
|
||||
"yaml": "^2.2.2"
|
||||
|
|
@ -852,9 +852,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
|
@ -1517,9 +1517,9 @@
|
|||
"link": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.0.tgz",
|
||||
"integrity": "sha512-jwXtxYbRt1V+CdQSy6Z+uZti7JF5irRKF8hlKfEnF/xJpcNGuuiZMBvuoYM+x9sr9iWGnzrlM0+9hvQ1kgkf1w==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz",
|
||||
"integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -1529,9 +1529,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.0.tgz",
|
||||
"integrity": "sha512-fI9nduZhCccjzlsA/OuAwtFGWocxA4gqXGTLvOyiF8d+8o0fZUeSztixkYjcGq1fGZY3Tkq4yRvHPFxU+jdZ9Q==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz",
|
||||
"integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1541,9 +1541,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.0.tgz",
|
||||
"integrity": "sha512-BcnSPRM76/cD2gQC+rQNGBN6GStBs2pl/FpweW8JYuz5J/IEa0Fr4AtrPv766DB/6b2MZ/AfSIOSGw3nEIP8SA==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz",
|
||||
"integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1553,9 +1553,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.0.tgz",
|
||||
"integrity": "sha512-LDyFB9GRolGN7XI6955aFeI3wCdCUszFWumWU0deHA8VpR3nWRrjG6GtGjBrQxQKFevnUTHKCfPR4IvrW3kCgQ==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz",
|
||||
"integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1565,9 +1565,21 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.0.tgz",
|
||||
"integrity": "sha512-ygrGVhQP47mRh0AAD0zl6QqCbNsf0eTo+vgwkY6LunBcg0f2Jv365GXlDUECIyoXp1kKwL5WW6rsO429DBY/bA==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz",
|
||||
"integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz",
|
||||
"integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -1577,9 +1589,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.0.tgz",
|
||||
"integrity": "sha512-x+uJ6MAYRlHGe9wi4HQjxpaKHPM3d3JjqqCkeC5gpnnI6OWovLdXTpfa8trjxPLnWKyBsSi5kne+146GAxFt4A==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1589,9 +1601,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.0.tgz",
|
||||
"integrity": "sha512-nrRw8ZTQKg6+Lttwqo6a2VxR9tOroa2m91XbdQ2sUUzHoedXlsyvY1fN4xWdqz8PKmf4orDwejxXHjh7YBGUCA==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz",
|
||||
"integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1601,11 +1613,11 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.0.tgz",
|
||||
"integrity": "sha512-xV0d5jDb4aFu84XKr+lcUJ9y3qpIWhttO3Qev97z8DKLXR62LC3cXT/bMZXrjLF9X+P5oSmJTzAhqwUbY96PnA==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==",
|
||||
"cpu": [
|
||||
"ppc64le"
|
||||
"ppc64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1613,9 +1625,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.0.tgz",
|
||||
"integrity": "sha512-SDDhBQwZX6LPRoPYjAZWyL27LbcBo7WdBFWJi5PI9RPCzU8ijzkQn7tt8NXiXRiFMJCVpkuMkBf4OxSxVMizAw==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
|
|
@ -1625,9 +1637,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.0.tgz",
|
||||
"integrity": "sha512-RxB/qez8zIDshNJDufYlTT0ZTVut5eCpAZ3bdXDU9yTxBzui3KhbGjROK2OYTTor7alM7XBhssgoO3CZ0XD3qA==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
|
|
@ -1637,9 +1649,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.0.tgz",
|
||||
"integrity": "sha512-C6y6z2eCNCfhZxT9u+jAM2Fup89ZjiG5pIzZIDycs1IwESviLxwkQcFRGLjnDrP+PT+v5i4YFvlcfAs+LnreXg==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz",
|
||||
"integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1649,9 +1661,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.0.tgz",
|
||||
"integrity": "sha512-i0QwbHYfnOMYsBEyjxcwGu5SMIi9sImDVjDg087hpzXqhBSosxkE7gyIYFHgfFl4mr7RrXksIBZ4DoLoP4FhJg==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz",
|
||||
"integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1661,9 +1673,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.0.tgz",
|
||||
"integrity": "sha512-Fq52EYb0riNHLBTAcL0cun+rRwyZ10S9vKzhGKKgeD+XbwunszSY0rVMco5KbOsTlwovP2rTOkiII/fQ4ih/zQ==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz",
|
||||
"integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1673,9 +1685,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.0.tgz",
|
||||
"integrity": "sha512-e/PBHxPdJ00O9p5Ui43+vixSgVf4NlLsmV6QneGERJ3lnjIua/kim6PRFe3iDueT1rQcgSkYP8ZBBXa/h4iPvw==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz",
|
||||
"integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
|
@ -1685,9 +1697,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.0.tgz",
|
||||
"integrity": "sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz",
|
||||
"integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -2876,9 +2888,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.5.tgz",
|
||||
"integrity": "sha512-RuLrmzYrxSb0s9SgpB+QN5jJucPduZQ/9SIe76MDxYJuecPW5mxMdacJ1f4EtgiV+R0p3sCkznTMvH0MPGFqjA==",
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.7.1.tgz",
|
||||
"integrity": "sha512-am9lR+HidiBtPtEYV7aFBpFJaQZhwJbYKr37cOHw0GGR+uiG0O79f20JNLjR0qEwPMuxOHvdBu4HHfimClBOCg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mitt": "3.0.1",
|
||||
|
|
@ -5862,9 +5874,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
|
|
@ -5917,9 +5929,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.38",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
|
||||
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
|
@ -5936,8 +5948,8 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.2.0"
|
||||
"picocolors": "^1.1.0",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
|
|
@ -6301,9 +6313,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.0.tgz",
|
||||
"integrity": "sha512-Qe7w62TyawbDzB4yt32R0+AbIo6m1/sqO7UPzFS8Z/ksL5mrfhA0v4CavfdmFav3D+ub4QeAgsGEe84DoWe/nQ==",
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz",
|
||||
"integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.5"
|
||||
},
|
||||
|
|
@ -6315,21 +6327,22 @@
|
|||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.14.0",
|
||||
"@rollup/rollup-android-arm64": "4.14.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.14.0",
|
||||
"@rollup/rollup-darwin-x64": "4.14.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.14.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.14.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.14.0",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.14.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.14.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.14.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.14.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.14.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.14.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.14.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.14.0",
|
||||
"@rollup/rollup-android-arm-eabi": "4.21.3",
|
||||
"@rollup/rollup-android-arm64": "4.21.3",
|
||||
"@rollup/rollup-darwin-arm64": "4.21.3",
|
||||
"@rollup/rollup-darwin-x64": "4.21.3",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.21.3",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.21.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.21.3",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.21.3",
|
||||
"@rollup/rollup-linux-x64-musl": "4.21.3",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.21.3",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.21.3",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.21.3",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
|
|
@ -6589,9 +6602,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -7171,13 +7184,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz",
|
||||
"integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==",
|
||||
"version": "5.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
|
||||
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.20.1",
|
||||
"postcss": "^8.4.38",
|
||||
"rollup": "^4.13.0"
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
|
|
@ -7196,6 +7209,7 @@
|
|||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
|
|
@ -7213,6 +7227,9 @@
|
|||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
|
|
@ -7243,9 +7260,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -7258,9 +7275,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -7273,9 +7290,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -7288,9 +7305,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -7303,9 +7320,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -7318,9 +7335,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -7333,9 +7350,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -7348,9 +7365,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -7363,9 +7380,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -7378,9 +7395,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
|
@ -7393,9 +7410,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
|
||||
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
||||
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
|
|
@ -7408,9 +7425,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
|
||||
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
||||
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
|
|
@ -7423,9 +7440,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
|
@ -7438,9 +7455,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
|
||||
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
||||
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
|
|
@ -7453,9 +7470,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
|
||||
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
||||
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
|
|
@ -7468,9 +7485,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -7483,9 +7500,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -7498,9 +7515,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -7513,9 +7530,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -7528,9 +7545,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -7543,9 +7560,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
|
@ -7558,9 +7575,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -7573,9 +7590,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/esbuild": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
|
|
@ -7584,29 +7601,29 @@
|
|||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.20.2",
|
||||
"@esbuild/android-arm": "0.20.2",
|
||||
"@esbuild/android-arm64": "0.20.2",
|
||||
"@esbuild/android-x64": "0.20.2",
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@esbuild/freebsd-arm64": "0.20.2",
|
||||
"@esbuild/freebsd-x64": "0.20.2",
|
||||
"@esbuild/linux-arm": "0.20.2",
|
||||
"@esbuild/linux-arm64": "0.20.2",
|
||||
"@esbuild/linux-ia32": "0.20.2",
|
||||
"@esbuild/linux-loong64": "0.20.2",
|
||||
"@esbuild/linux-mips64el": "0.20.2",
|
||||
"@esbuild/linux-ppc64": "0.20.2",
|
||||
"@esbuild/linux-riscv64": "0.20.2",
|
||||
"@esbuild/linux-s390x": "0.20.2",
|
||||
"@esbuild/linux-x64": "0.20.2",
|
||||
"@esbuild/netbsd-x64": "0.20.2",
|
||||
"@esbuild/openbsd-x64": "0.20.2",
|
||||
"@esbuild/sunos-x64": "0.20.2",
|
||||
"@esbuild/win32-arm64": "0.20.2",
|
||||
"@esbuild/win32-ia32": "0.20.2",
|
||||
"@esbuild/win32-x64": "0.20.2"
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
}
|
||||
},
|
||||
"node_modules/vitefu": {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
"@zip.js/zip.js": "^2.7.29",
|
||||
"ansi-styles": "^4.3.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"chromium-bidi": "^0.6.4",
|
||||
"chromium-bidi": "^0.7.1",
|
||||
"colors": "^1.4.0",
|
||||
"concurrently": "^6.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
|
|
@ -100,7 +100,7 @@
|
|||
"react-dom": "^18.1.0",
|
||||
"ssim.js": "^3.5.0",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.0.13",
|
||||
"vite": "^5.4.6",
|
||||
"ws": "^8.17.1",
|
||||
"xml2js": "^0.5.0",
|
||||
"yaml": "^2.2.2"
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@
|
|||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1135",
|
||||
"revision": "1136",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "129.0.6668.42"
|
||||
"browserVersion": "130.0.6723.6"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1259",
|
||||
"revision": "1261",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "130.0.6713.0"
|
||||
"browserVersion": "131.0.6726.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2077",
|
||||
"revision": "2080",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac10.14": "1446",
|
||||
|
|
|
|||
|
|
@ -397,7 +397,7 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
process.stdout.write('\n-------------8<-------------\n');
|
||||
const autoExitCondition = process.env.PWTEST_CLI_AUTO_EXIT_WHEN;
|
||||
if (autoExitCondition && text.includes(autoExitCondition))
|
||||
Promise.all(context.pages().map(async p => p.close()));
|
||||
closeBrowser();
|
||||
};
|
||||
// Make sure we exit abnormally when browser crashes.
|
||||
const logs: string[] = [];
|
||||
|
|
@ -504,7 +504,7 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
if (hasPage)
|
||||
return;
|
||||
// Avoid the error when the last page is closed because the browser has been closed.
|
||||
closeBrowser().catch(e => null);
|
||||
closeBrowser().catch(() => {});
|
||||
});
|
||||
});
|
||||
process.on('SIGINT', async () => {
|
||||
|
|
@ -560,17 +560,13 @@ async function open(options: Options, url: string | undefined, language: string)
|
|||
|
||||
async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) {
|
||||
const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options;
|
||||
const tracesDir = path.join(os.tmpdir(), `recorder-trace-${Date.now()}`);
|
||||
const tracesDir = path.join(os.tmpdir(), `playwright-recorder-trace-${Date.now()}`);
|
||||
const { context, launchOptions, contextOptions } = await launchContext(options, {
|
||||
headless: !!process.env.PWTEST_CLI_HEADLESS,
|
||||
executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH,
|
||||
tracesDir,
|
||||
});
|
||||
dotenv.config({ path: 'playwright.env' });
|
||||
if (process.env.PW_RECORDER_IS_TRACE_VIEWER) {
|
||||
await fs.promises.mkdir(tracesDir, { recursive: true });
|
||||
await context.tracing.start({ name: 'trace', _live: true });
|
||||
}
|
||||
await context._enableRecorder({
|
||||
language,
|
||||
launchOptions,
|
||||
|
|
@ -578,6 +574,7 @@ async function codegen(options: Options & { target: string, output?: string, tes
|
|||
device: options.device,
|
||||
saveStorage: options.saveStorage,
|
||||
mode: 'recording',
|
||||
codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions',
|
||||
testIdAttributeName,
|
||||
outputFile: outputFile ? path.resolve(outputFile) : undefined,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export { TimeoutError } from './errors';
|
|||
export { Frame } from './frame';
|
||||
export { Keyboard, Mouse, Touchscreen } from './input';
|
||||
export { JSHandle } from './jsHandle';
|
||||
export { Request, Response, Route, WebSocket } from './network';
|
||||
export { Request, Response, Route, WebSocket, WebSocketRoute } from './network';
|
||||
export { APIRequest, APIRequestContext, APIResponse } from './fetch';
|
||||
export { Page } from './page';
|
||||
export { Selectors } from './selectors';
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import { Clock } from './clock';
|
|||
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
|
||||
_pages = new Set<Page>();
|
||||
_routes: network.RouteHandler[] = [];
|
||||
_webSocketRoutes: network.WebSocketRouteHandler[] = [];
|
||||
readonly _browser: Browser | null = null;
|
||||
_browserType: BrowserType | undefined;
|
||||
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
|
||||
|
|
@ -90,6 +91,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
this._channel.on('close', () => this._onClose());
|
||||
this._channel.on('page', ({ page }) => this._onPage(Page.from(page)));
|
||||
this._channel.on('route', ({ route }) => this._onRoute(network.Route.from(route)));
|
||||
this._channel.on('webSocketRoute', ({ webSocketRoute }) => this._onWebSocketRoute(network.WebSocketRoute.from(webSocketRoute)));
|
||||
this._channel.on('backgroundPage', ({ page }) => {
|
||||
const backgroundPage = Page.from(page);
|
||||
this._backgroundPages.add(backgroundPage);
|
||||
|
|
@ -218,7 +220,15 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
}
|
||||
// If the page is closed or unrouteAll() was called without waiting and interception disabled,
|
||||
// the method will throw an error - silence it.
|
||||
await route._innerContinue(true).catch(() => {});
|
||||
await route._innerContinue(true /* isFallback */).catch(() => {});
|
||||
}
|
||||
|
||||
async _onWebSocketRoute(webSocketRoute: network.WebSocketRoute) {
|
||||
const routeHandler = this._webSocketRoutes.find(route => route.matches(webSocketRoute.url()));
|
||||
if (routeHandler)
|
||||
await routeHandler.handle(webSocketRoute);
|
||||
else
|
||||
await webSocketRoute.connect();
|
||||
}
|
||||
|
||||
async _onBinding(bindingCall: BindingCall) {
|
||||
|
|
@ -328,6 +338,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
await this._updateInterceptionPatterns();
|
||||
}
|
||||
|
||||
async routeWebSocket(url: URLMatch, handler: network.WebSocketRouteHandlerCallback): Promise<void> {
|
||||
this._webSocketRoutes.unshift(new network.WebSocketRouteHandler(this._options.baseURL, url, handler));
|
||||
await this._updateWebSocketInterceptionPatterns();
|
||||
}
|
||||
|
||||
async _recordIntoHAR(har: string, page: Page | null, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full'} = {}): Promise<void> {
|
||||
const { harId } = await this._channel.harStart({
|
||||
page: page?._channel,
|
||||
|
|
@ -387,6 +402,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
await this._channel.setNetworkInterceptionPatterns({ patterns });
|
||||
}
|
||||
|
||||
private async _updateWebSocketInterceptionPatterns() {
|
||||
const patterns = network.WebSocketRouteHandler.prepareInterceptionPatterns(this._webSocketRoutes);
|
||||
await this._channel.setWebSocketInterceptionPatterns({ patterns });
|
||||
}
|
||||
|
||||
_effectiveCloseReason(): string | undefined {
|
||||
return this._closeReason || this._browser?._closeReason;
|
||||
}
|
||||
|
|
@ -472,17 +492,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
await this._closedPromise;
|
||||
}
|
||||
|
||||
async _enableRecorder(params: {
|
||||
language: string,
|
||||
launchOptions?: LaunchOptions,
|
||||
contextOptions?: BrowserContextOptions,
|
||||
device?: string,
|
||||
saveStorage?: string,
|
||||
mode?: 'recording' | 'inspecting',
|
||||
testIdAttributeName?: string,
|
||||
outputFile?: string,
|
||||
}) {
|
||||
await this._channel.recorderSupplementEnable(params);
|
||||
async _enableRecorder(params: channels.BrowserContextEnableRecorderParams) {
|
||||
await this._channel.enableRecorder(params);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
_logger: Logger | undefined;
|
||||
readonly _instrumentation: ClientInstrumentation;
|
||||
private _eventToSubscriptionMapping: Map<string, string> = new Map();
|
||||
private _isInternalType = false;
|
||||
_wasCollected: boolean = false;
|
||||
|
||||
constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits<T>) {
|
||||
|
|
@ -61,6 +62,10 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
this._initializer = initializer;
|
||||
}
|
||||
|
||||
protected markAsInternalType() {
|
||||
this._isInternalType = true;
|
||||
}
|
||||
|
||||
_setEventToSubscriptionMapping(mapping: Map<string, string>) {
|
||||
this._eventToSubscriptionMapping = mapping;
|
||||
}
|
||||
|
|
@ -173,7 +178,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
let apiName: string | undefined = stackTrace.apiName;
|
||||
const frames: channels.StackFrame[] = stackTrace.frames;
|
||||
|
||||
isInternal = isInternal || this._type === 'LocalUtils';
|
||||
isInternal = isInternal || this._isInternalType;
|
||||
if (isInternal)
|
||||
apiName = undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { ChannelOwner } from './channelOwner';
|
|||
import { ElementHandle } from './elementHandle';
|
||||
import { Frame } from './frame';
|
||||
import { JSHandle } from './jsHandle';
|
||||
import { Request, Response, Route, WebSocket } from './network';
|
||||
import { Request, Response, Route, WebSocket, WebSocketRoute } from './network';
|
||||
import { Page, BindingCall } from './page';
|
||||
import { Worker } from './worker';
|
||||
import { Dialog } from './dialog';
|
||||
|
|
@ -309,6 +309,9 @@ export class Connection extends EventEmitter {
|
|||
case 'WebSocket':
|
||||
result = new WebSocket(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'WebSocketRoute':
|
||||
result = new WebSocketRoute(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Worker':
|
||||
result = new Worker(parent, type, guid, initializer);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ export const Events = {
|
|||
FrameSent: 'framesent',
|
||||
},
|
||||
|
||||
WebSocketRoute: {
|
||||
Close: 'close',
|
||||
},
|
||||
|
||||
Worker: {
|
||||
Close: 'close',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
|
|||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this.markAsInternalType();
|
||||
this.devices = {};
|
||||
for (const { name, descriptor } of initializer.deviceDescriptors)
|
||||
this.devices[name] = descriptor;
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.RouteInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this.markAsInternalType();
|
||||
}
|
||||
|
||||
request(): Request {
|
||||
|
|
@ -325,7 +326,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
|
||||
async abort(errorCode?: string) {
|
||||
await this._handleRoute(async () => {
|
||||
await this._raceWithTargetClose(this._channel.abort({ requestUrl: this.request()._initializer.url, errorCode }));
|
||||
await this._raceWithTargetClose(this._channel.abort({ errorCode }));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -409,7 +410,6 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
headers['content-length'] = String(length);
|
||||
|
||||
await this._raceWithTargetClose(this._channel.fulfill({
|
||||
requestUrl: this.request()._initializer.url,
|
||||
status: statusOption || 200,
|
||||
headers: headersObjectToArray(headers),
|
||||
body,
|
||||
|
|
@ -421,7 +421,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
async continue(options: FallbackOverrides = {}) {
|
||||
await this._handleRoute(async () => {
|
||||
this.request()._applyFallbackOverrides(options);
|
||||
await this._innerContinue();
|
||||
await this._innerContinue(false /* isFallback */);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -436,22 +436,143 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
chain.resolve(done);
|
||||
}
|
||||
|
||||
async _innerContinue(internal = false) {
|
||||
async _innerContinue(isFallback: boolean) {
|
||||
const options = this.request()._fallbackOverridesForContinue();
|
||||
return await this._wrapApiCall(async () => {
|
||||
await this._raceWithTargetClose(this._channel.continue({
|
||||
requestUrl: this.request()._initializer.url,
|
||||
return await this._raceWithTargetClose(this._channel.continue({
|
||||
url: options.url,
|
||||
method: options.method,
|
||||
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
|
||||
postData: options.postDataBuffer,
|
||||
isFallback: internal,
|
||||
isFallback,
|
||||
}));
|
||||
}, !!internal);
|
||||
}
|
||||
}
|
||||
|
||||
export class WebSocketRoute extends ChannelOwner<channels.WebSocketRouteChannel> implements api.WebSocketRoute {
|
||||
static from(route: channels.WebSocketRouteChannel): WebSocketRoute {
|
||||
return (route as any)._object;
|
||||
}
|
||||
|
||||
private _routeSendHandler?: (message: string | Buffer) => any;
|
||||
private _routeReceiveHandler?: (message: string | Buffer) => any;
|
||||
private _connected = false;
|
||||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.WebSocketRouteInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
|
||||
this._channel.on('messageFromPage', ({ message, isBase64 }) => {
|
||||
if (this._routeSendHandler)
|
||||
this._routeSendHandler(isBase64 ? Buffer.from(message, 'base64') : message);
|
||||
else
|
||||
this._channel.sendToServer({ message, isBase64 }).catch(() => {});
|
||||
});
|
||||
|
||||
this._channel.on('messageFromServer', ({ message, isBase64 }) => {
|
||||
if (this._routeReceiveHandler)
|
||||
this._routeReceiveHandler(isBase64 ? Buffer.from(message, 'base64') : message);
|
||||
else
|
||||
this._channel.sendToPage({ message, isBase64 }).catch(() => {});
|
||||
});
|
||||
|
||||
this._channel.on('close', () => this.emit(Events.WebSocketRoute.Close));
|
||||
}
|
||||
|
||||
url() {
|
||||
return this._initializer.url;
|
||||
}
|
||||
|
||||
async close(options: { code?: number, reason?: string } = {}) {
|
||||
try {
|
||||
await this._channel.close(options);
|
||||
} catch (e) {
|
||||
if (isTargetClosedError(e))
|
||||
return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async connect() {
|
||||
this._connected = true;
|
||||
await this._channel.connect();
|
||||
}
|
||||
|
||||
send(message: string | Buffer) {
|
||||
if (isString(message))
|
||||
this._channel.sendToServer({ message, isBase64: false }).catch(() => {});
|
||||
else
|
||||
this._channel.sendToServer({ message: message.toString('base64'), isBase64: true }).catch(() => {});
|
||||
}
|
||||
|
||||
receive(message: string | Buffer) {
|
||||
if (isString(message))
|
||||
this._channel.sendToPage({ message, isBase64: false }).catch(() => {});
|
||||
else
|
||||
this._channel.sendToPage({ message: message.toString('base64'), isBase64: true }).catch(() => {});
|
||||
}
|
||||
|
||||
routeSend(handler: (message: string | Buffer) => any) {
|
||||
this._routeSendHandler = handler;
|
||||
}
|
||||
|
||||
routeReceive(handler: (message: string | Buffer) => any) {
|
||||
this._routeReceiveHandler = handler;
|
||||
}
|
||||
|
||||
async [Symbol.asyncDispose]() {
|
||||
await this.close();
|
||||
}
|
||||
|
||||
async _afterHandle() {
|
||||
if (this._connected)
|
||||
return;
|
||||
if (this._routeReceiveHandler)
|
||||
throw new Error(`WebSocketRoute.routeReceive() call had no effect. Make sure to call WebSocketRoute.connect() as well.`);
|
||||
// Ensure that websocket is "open", so that test can send messages to it
|
||||
// without an actual server connection.
|
||||
await this._channel.ensureOpened();
|
||||
}
|
||||
}
|
||||
|
||||
export class WebSocketRouteHandler {
|
||||
private readonly _baseURL: string | undefined;
|
||||
readonly url: URLMatch;
|
||||
readonly handler: WebSocketRouteHandlerCallback;
|
||||
|
||||
constructor(baseURL: string | undefined, url: URLMatch, handler: WebSocketRouteHandlerCallback) {
|
||||
this._baseURL = baseURL;
|
||||
this.url = url;
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
static prepareInterceptionPatterns(handlers: WebSocketRouteHandler[]) {
|
||||
const patterns: channels.BrowserContextSetWebSocketInterceptionPatternsParams['patterns'] = [];
|
||||
let all = false;
|
||||
for (const handler of handlers) {
|
||||
if (isString(handler.url))
|
||||
patterns.push({ glob: handler.url });
|
||||
else if (isRegExp(handler.url))
|
||||
patterns.push({ regexSource: handler.url.source, regexFlags: handler.url.flags });
|
||||
else
|
||||
all = true;
|
||||
}
|
||||
if (all)
|
||||
return [{ glob: '**/*' }];
|
||||
return patterns;
|
||||
}
|
||||
|
||||
public matches(wsURL: string): boolean {
|
||||
return urlMatches(this._baseURL, wsURL, this.url);
|
||||
}
|
||||
|
||||
public async handle(webSocketRoute: WebSocketRoute) {
|
||||
const handler = this.handler;
|
||||
await handler(webSocketRoute);
|
||||
await webSocketRoute._afterHandle();
|
||||
}
|
||||
}
|
||||
|
||||
export type RouteHandlerCallback = (route: Route, request: Request) => Promise<any> | void;
|
||||
export type WebSocketRouteHandlerCallback = (ws: WebSocketRoute) => Promise<any> | void;
|
||||
|
||||
export type ResourceTiming = {
|
||||
startTime: number;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import { Keyboard, Mouse, Touchscreen } from './input';
|
|||
import { assertMaxArguments, JSHandle, parseResult, serializeArgument } from './jsHandle';
|
||||
import type { FrameLocator, Locator, LocatorOptions } from './locator';
|
||||
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
|
||||
import { type RouteHandlerCallback, type Request, Response, Route, RouteHandler, validateHeaders, WebSocket } from './network';
|
||||
import { type RouteHandlerCallback, type Request, Response, Route, RouteHandler, validateHeaders, WebSocket, type WebSocketRouteHandlerCallback, WebSocketRoute, WebSocketRouteHandler } from './network';
|
||||
import type { FilePayload, Headers, LifecycleEvent, SelectOption, SelectOptionOptions, Size, WaitForEventOptions, WaitForFunctionOptions } from './types';
|
||||
import { Video } from './video';
|
||||
import { Waiter } from './waiter';
|
||||
|
|
@ -78,6 +78,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
readonly _closedOrCrashedScope = new LongStandingScope();
|
||||
private _viewportSize: Size | null;
|
||||
_routes: RouteHandler[] = [];
|
||||
_webSocketRoutes: WebSocketRouteHandler[] = [];
|
||||
|
||||
readonly accessibility: Accessibility;
|
||||
readonly coverage: Coverage;
|
||||
|
|
@ -137,6 +138,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
this._channel.on('frameDetached', ({ frame }) => this._onFrameDetached(Frame.from(frame)));
|
||||
this._channel.on('locatorHandlerTriggered', ({ uid }) => this._onLocatorHandlerTriggered(uid));
|
||||
this._channel.on('route', ({ route }) => this._onRoute(Route.from(route)));
|
||||
this._channel.on('webSocketRoute', ({ webSocketRoute }) => this._onWebSocketRoute(WebSocketRoute.from(webSocketRoute)));
|
||||
this._channel.on('video', ({ artifact }) => {
|
||||
const artifactObject = Artifact.from(artifact);
|
||||
this._forceVideo()._artifactReady(artifactObject);
|
||||
|
|
@ -200,6 +202,14 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
await this._browserContext._onRoute(route);
|
||||
}
|
||||
|
||||
private async _onWebSocketRoute(webSocketRoute: WebSocketRoute) {
|
||||
const routeHandler = this._webSocketRoutes.find(route => route.matches(webSocketRoute.url()));
|
||||
if (routeHandler)
|
||||
await routeHandler.handle(webSocketRoute);
|
||||
else
|
||||
await this._browserContext._onWebSocketRoute(webSocketRoute);
|
||||
}
|
||||
|
||||
async _onBinding(bindingCall: BindingCall) {
|
||||
const func = this._bindings.get(bindingCall._initializer.name);
|
||||
if (func) {
|
||||
|
|
@ -515,6 +525,11 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
await harRouter.addPageRoute(this);
|
||||
}
|
||||
|
||||
async routeWebSocket(url: URLMatch, handler: WebSocketRouteHandlerCallback): Promise<void> {
|
||||
this._webSocketRoutes.unshift(new WebSocketRouteHandler(this._browserContext._options.baseURL, url, handler));
|
||||
await this._updateWebSocketInterceptionPatterns();
|
||||
}
|
||||
|
||||
private _disposeHarRouters() {
|
||||
this._harRouters.forEach(router => router.dispose());
|
||||
this._harRouters = [];
|
||||
|
|
@ -551,6 +566,11 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
await this._channel.setNetworkInterceptionPatterns({ patterns });
|
||||
}
|
||||
|
||||
private async _updateWebSocketInterceptionPatterns() {
|
||||
const patterns = WebSocketRouteHandler.prepareInterceptionPatterns(this._webSocketRoutes);
|
||||
await this._channel.setWebSocketInterceptionPatterns({ patterns });
|
||||
}
|
||||
|
||||
async screenshot(options: Omit<channels.PageScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
|
||||
const copy: channels.PageScreenshotOptions = { ...options, mask: undefined };
|
||||
if (!copy.type)
|
||||
|
|
|
|||
|
|
@ -31,20 +31,18 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.TracingInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this.markAsInternalType();
|
||||
}
|
||||
|
||||
async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, _live?: boolean } = {}) {
|
||||
this._includeSources = !!options.sources;
|
||||
const traceName = await this._wrapApiCall(async () => {
|
||||
await this._channel.tracingStart({
|
||||
name: options.name,
|
||||
snapshots: options.snapshots,
|
||||
screenshots: options.screenshots,
|
||||
live: options._live,
|
||||
});
|
||||
const response = await this._channel.tracingStartChunk({ name: options.name, title: options.title });
|
||||
return response.traceName;
|
||||
}, true);
|
||||
const { traceName } = await this._channel.tracingStartChunk({ name: options.name, title: options.title });
|
||||
await this._startCollectingStacks(traceName);
|
||||
}
|
||||
|
||||
|
|
@ -63,16 +61,12 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
}
|
||||
|
||||
async stopChunk(options: { path?: string } = {}) {
|
||||
await this._wrapApiCall(async () => {
|
||||
await this._doStopChunk(options.path);
|
||||
}, true);
|
||||
}
|
||||
|
||||
async stop(options: { path?: string } = {}) {
|
||||
await this._wrapApiCall(async () => {
|
||||
await this._doStopChunk(options.path);
|
||||
await this._channel.tracingStop();
|
||||
}, true);
|
||||
}
|
||||
|
||||
private async _doStopChunk(filePath: string | undefined) {
|
||||
|
|
|
|||
|
|
@ -832,6 +832,9 @@ scheme.BrowserContextPageErrorEvent = tObject({
|
|||
scheme.BrowserContextRouteEvent = tObject({
|
||||
route: tChannel(['Route']),
|
||||
});
|
||||
scheme.BrowserContextWebSocketRouteEvent = tObject({
|
||||
webSocketRoute: tChannel(['WebSocketRoute']),
|
||||
});
|
||||
scheme.BrowserContextVideoEvent = tObject({
|
||||
artifact: tChannel(['Artifact']),
|
||||
});
|
||||
|
|
@ -943,6 +946,14 @@ scheme.BrowserContextSetNetworkInterceptionPatternsParams = tObject({
|
|||
})),
|
||||
});
|
||||
scheme.BrowserContextSetNetworkInterceptionPatternsResult = tOptional(tObject({}));
|
||||
scheme.BrowserContextSetWebSocketInterceptionPatternsParams = tObject({
|
||||
patterns: tArray(tObject({
|
||||
glob: tOptional(tString),
|
||||
regexSource: tOptional(tString),
|
||||
regexFlags: tOptional(tString),
|
||||
})),
|
||||
});
|
||||
scheme.BrowserContextSetWebSocketInterceptionPatternsResult = tOptional(tObject({}));
|
||||
scheme.BrowserContextSetOfflineParams = tObject({
|
||||
offline: tBoolean,
|
||||
});
|
||||
|
|
@ -954,9 +965,10 @@ scheme.BrowserContextStorageStateResult = tObject({
|
|||
});
|
||||
scheme.BrowserContextPauseParams = tOptional(tObject({}));
|
||||
scheme.BrowserContextPauseResult = tOptional(tObject({}));
|
||||
scheme.BrowserContextRecorderSupplementEnableParams = tObject({
|
||||
scheme.BrowserContextEnableRecorderParams = tObject({
|
||||
language: tOptional(tString),
|
||||
mode: tOptional(tEnum(['inspecting', 'recording'])),
|
||||
codegenMode: tOptional(tEnum(['actions', 'trace-events'])),
|
||||
pauseOnNextStatement: tOptional(tBoolean),
|
||||
testIdAttributeName: tOptional(tString),
|
||||
launchOptions: tOptional(tAny),
|
||||
|
|
@ -966,7 +978,7 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({
|
|||
outputFile: tOptional(tString),
|
||||
omitCallTracking: tOptional(tBoolean),
|
||||
});
|
||||
scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({}));
|
||||
scheme.BrowserContextEnableRecorderResult = tOptional(tObject({}));
|
||||
scheme.BrowserContextNewCDPSessionParams = tObject({
|
||||
page: tOptional(tChannel(['Page'])),
|
||||
frame: tOptional(tChannel(['Frame'])),
|
||||
|
|
@ -1070,6 +1082,9 @@ scheme.PageLocatorHandlerTriggeredEvent = tObject({
|
|||
scheme.PageRouteEvent = tObject({
|
||||
route: tChannel(['Route']),
|
||||
});
|
||||
scheme.PageWebSocketRouteEvent = tObject({
|
||||
webSocketRoute: tChannel(['WebSocketRoute']),
|
||||
});
|
||||
scheme.PageVideoEvent = tObject({
|
||||
artifact: tChannel(['Artifact']),
|
||||
});
|
||||
|
|
@ -1211,6 +1226,14 @@ scheme.PageSetNetworkInterceptionPatternsParams = tObject({
|
|||
})),
|
||||
});
|
||||
scheme.PageSetNetworkInterceptionPatternsResult = tOptional(tObject({}));
|
||||
scheme.PageSetWebSocketInterceptionPatternsParams = tObject({
|
||||
patterns: tArray(tObject({
|
||||
glob: tOptional(tString),
|
||||
regexSource: tOptional(tString),
|
||||
regexFlags: tOptional(tString),
|
||||
})),
|
||||
});
|
||||
scheme.PageSetWebSocketInterceptionPatternsResult = tOptional(tObject({}));
|
||||
scheme.PageSetViewportSizeParams = tObject({
|
||||
viewportSize: tObject({
|
||||
width: tNumber,
|
||||
|
|
@ -2093,7 +2116,6 @@ scheme.RouteRedirectNavigationRequestParams = tObject({
|
|||
scheme.RouteRedirectNavigationRequestResult = tOptional(tObject({}));
|
||||
scheme.RouteAbortParams = tObject({
|
||||
errorCode: tOptional(tString),
|
||||
requestUrl: tString,
|
||||
});
|
||||
scheme.RouteAbortResult = tOptional(tObject({}));
|
||||
scheme.RouteContinueParams = tObject({
|
||||
|
|
@ -2101,7 +2123,6 @@ scheme.RouteContinueParams = tObject({
|
|||
method: tOptional(tString),
|
||||
headers: tOptional(tArray(tType('NameValue'))),
|
||||
postData: tOptional(tBinary),
|
||||
requestUrl: tString,
|
||||
isFallback: tBoolean,
|
||||
});
|
||||
scheme.RouteContinueResult = tOptional(tObject({}));
|
||||
|
|
@ -2111,9 +2132,39 @@ scheme.RouteFulfillParams = tObject({
|
|||
body: tOptional(tString),
|
||||
isBase64: tOptional(tBoolean),
|
||||
fetchResponseUid: tOptional(tString),
|
||||
requestUrl: tString,
|
||||
});
|
||||
scheme.RouteFulfillResult = tOptional(tObject({}));
|
||||
scheme.WebSocketRouteInitializer = tObject({
|
||||
url: tString,
|
||||
});
|
||||
scheme.WebSocketRouteMessageFromPageEvent = tObject({
|
||||
message: tString,
|
||||
isBase64: tBoolean,
|
||||
});
|
||||
scheme.WebSocketRouteMessageFromServerEvent = tObject({
|
||||
message: tString,
|
||||
isBase64: tBoolean,
|
||||
});
|
||||
scheme.WebSocketRouteCloseEvent = tOptional(tObject({}));
|
||||
scheme.WebSocketRouteConnectParams = tOptional(tObject({}));
|
||||
scheme.WebSocketRouteConnectResult = tOptional(tObject({}));
|
||||
scheme.WebSocketRouteEnsureOpenedParams = tOptional(tObject({}));
|
||||
scheme.WebSocketRouteEnsureOpenedResult = tOptional(tObject({}));
|
||||
scheme.WebSocketRouteSendToPageParams = tObject({
|
||||
message: tString,
|
||||
isBase64: tBoolean,
|
||||
});
|
||||
scheme.WebSocketRouteSendToPageResult = tOptional(tObject({}));
|
||||
scheme.WebSocketRouteSendToServerParams = tObject({
|
||||
message: tString,
|
||||
isBase64: tBoolean,
|
||||
});
|
||||
scheme.WebSocketRouteSendToServerResult = tOptional(tObject({}));
|
||||
scheme.WebSocketRouteCloseParams = tObject({
|
||||
code: tOptional(tNumber),
|
||||
reason: tOptional(tString),
|
||||
});
|
||||
scheme.WebSocketRouteCloseResult = tOptional(tObject({}));
|
||||
scheme.ResourceTiming = tObject({
|
||||
startTime: tNumber,
|
||||
domainLookupStart: tNumber,
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export class BidiConnection {
|
|||
let context;
|
||||
if ('context' in object.params)
|
||||
context = object.params.context;
|
||||
else if (object.method === 'log.entryAdded')
|
||||
else if (object.method === 'log.entryAdded' || object.method === 'script.message')
|
||||
context = object.params.source?.context;
|
||||
if (context) {
|
||||
const session = this._browsingContextToSession.get(context);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { BidiSerializer } from './third_party/bidiSerializer';
|
|||
|
||||
export class BidiExecutionContext implements js.ExecutionContextDelegate {
|
||||
private readonly _session: BidiSession;
|
||||
private readonly _target: bidi.Script.Target;
|
||||
readonly _target: bidi.Script.Target;
|
||||
|
||||
constructor(session: BidiSession, realmInfo: bidi.Script.RealmInfo) {
|
||||
this._session = session;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,14 @@ export class BidiFirefox extends BrowserType {
|
|||
override amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
|
||||
if (!path.isAbsolute(os.homedir()))
|
||||
throw new Error(`Cannot launch Firefox with relative home directory. Did you set ${os.platform() === 'win32' ? 'USERPROFILE' : 'HOME'} to a relative path?`);
|
||||
|
||||
env = {
|
||||
...env,
|
||||
'MOZ_CRASHREPORTER': '1',
|
||||
'MOZ_CRASHREPORTER_NO_REPORT': '1',
|
||||
'MOZ_CRASHREPORTER_SHUTDOWN': '1',
|
||||
};
|
||||
|
||||
if (os.platform() === 'linux') {
|
||||
// Always remove SNAP_NAME and SNAP_INSTANCE_NAME env variables since they
|
||||
// confuse Firefox: in our case, builds never come from SNAP.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ import type * as accessibility from '../accessibility';
|
|||
import * as dom from '../dom';
|
||||
import * as dialog from '../dialog';
|
||||
import type * as frames from '../frames';
|
||||
import { type InitScript, Page, type PageDelegate } from '../page';
|
||||
import { Page } from '../page';
|
||||
import type { InitScript, PageDelegate } from '../page';
|
||||
import type { Progress } from '../progress';
|
||||
import type * as types from '../types';
|
||||
import type { BidiBrowserContext } from './bidiBrowser';
|
||||
|
|
@ -33,6 +34,7 @@ import { BidiNetworkManager } from './bidiNetworkManager';
|
|||
import { BrowserContext } from '../browserContext';
|
||||
|
||||
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||
const kPlaywrightBindingChannel = 'playwrightChannel';
|
||||
|
||||
export class BidiPage implements PageDelegate {
|
||||
readonly rawMouse: RawMouseImpl;
|
||||
|
|
@ -62,6 +64,7 @@ export class BidiPage implements PageDelegate {
|
|||
this._page.on(Page.Events.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false));
|
||||
this._sessionListeners = [
|
||||
eventsHelper.addEventListener(bidiSession, 'script.realmCreated', this._onRealmCreated.bind(this)),
|
||||
eventsHelper.addEventListener(bidiSession, 'script.message', this._onScriptMessage.bind(this)),
|
||||
eventsHelper.addEventListener(bidiSession, 'browsingContext.contextDestroyed', this._onBrowsingContextDestroyed.bind(this)),
|
||||
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationStarted', this._onNavigationStarted.bind(this)),
|
||||
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationAborted', this._onNavigationAborted.bind(this)),
|
||||
|
|
@ -93,6 +96,7 @@ export class BidiPage implements PageDelegate {
|
|||
this.updateHttpCredentials(),
|
||||
this.updateRequestInterception(),
|
||||
this._updateViewport(),
|
||||
this._installMainBinding(),
|
||||
this._addAllInitScripts(),
|
||||
]);
|
||||
}
|
||||
|
|
@ -315,18 +319,63 @@ export class BidiPage implements PageDelegate {
|
|||
});
|
||||
}
|
||||
|
||||
goBack(): Promise<boolean> {
|
||||
throw new Error('Method not implemented.');
|
||||
async goBack(): Promise<boolean> {
|
||||
return await this._session.send('browsingContext.traverseHistory', {
|
||||
context: this._session.sessionId,
|
||||
delta: -1,
|
||||
}).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
goForward(): Promise<boolean> {
|
||||
throw new Error('Method not implemented.');
|
||||
async goForward(): Promise<boolean> {
|
||||
return await this._session.send('browsingContext.traverseHistory', {
|
||||
context: this._session.sessionId,
|
||||
delta: +1,
|
||||
}).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
async forceGarbageCollection(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
// TODO: consider calling this only when bindings are added.
|
||||
private async _installMainBinding() {
|
||||
const functionDeclaration = addMainBinding.toString();
|
||||
const args: bidi.Script.ChannelValue[] = [{
|
||||
type: 'channel',
|
||||
value: {
|
||||
channel: kPlaywrightBindingChannel,
|
||||
ownership: bidi.Script.ResultOwnership.Root,
|
||||
}
|
||||
}];
|
||||
const promises = [];
|
||||
promises.push(this._session.send('script.addPreloadScript', {
|
||||
functionDeclaration,
|
||||
arguments: args,
|
||||
}));
|
||||
promises.push(this._session.send('script.callFunction', {
|
||||
functionDeclaration,
|
||||
arguments: args,
|
||||
target: toBidiExecutionContext(await this._page.mainFrame()._mainContext())._target,
|
||||
awaitPromise: false,
|
||||
userActivation: false,
|
||||
}));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
private async _onScriptMessage(event: bidi.Script.MessageParameters) {
|
||||
if (event.channel !== kPlaywrightBindingChannel)
|
||||
return;
|
||||
const pageOrError = await this.pageOrError();
|
||||
if (pageOrError instanceof Error)
|
||||
return;
|
||||
const context = this._realmToContext.get(event.source.realm);
|
||||
if (!context)
|
||||
return;
|
||||
if (event.data.type !== 'string')
|
||||
return;
|
||||
await this._page._onBindingCalled(event.data.value, context);
|
||||
}
|
||||
|
||||
async addInitScript(initScript: InitScript): Promise<void> {
|
||||
const { script } = await this._session.send('script.addPreloadScript', {
|
||||
// TODO: remove function call from the source.
|
||||
|
|
@ -355,7 +404,20 @@ export class BidiPage implements PageDelegate {
|
|||
}
|
||||
|
||||
async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
|
||||
throw new Error('Method not implemented.');
|
||||
const rect = (documentRect || viewportRect)!;
|
||||
const { data } = await this._session.send('browsingContext.captureScreenshot', {
|
||||
context: this._session.sessionId,
|
||||
format: {
|
||||
type: `image/${format === 'png' ? 'png' : 'jpeg'}`,
|
||||
quality: quality || 80,
|
||||
},
|
||||
origin: documentRect ? 'document' : 'viewport',
|
||||
clip: {
|
||||
type: 'box',
|
||||
...rect,
|
||||
}
|
||||
});
|
||||
return Buffer.from(data, 'base64');
|
||||
}
|
||||
|
||||
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
|
||||
|
|
@ -522,6 +584,10 @@ export class BidiPage implements PageDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
function addMainBinding(callback: (arg: any) => void) {
|
||||
(globalThis as any)['__playwright__binding__'] = callback;
|
||||
}
|
||||
|
||||
function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext {
|
||||
return (executionContext as any)[contextDelegateSymbol] as BidiExecutionContext;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,15 +131,15 @@ export abstract class BrowserContext extends SdkObject {
|
|||
|
||||
// When PWDEBUG=1, show inspector for each context.
|
||||
if (debugMode() === 'inspector')
|
||||
await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true });
|
||||
await Recorder.show('actions', this, RecorderApp.factory(this), { pauseOnNextStatement: true });
|
||||
|
||||
// When paused, show inspector.
|
||||
if (this._debugger.isPaused())
|
||||
Recorder.showInspector(this, RecorderApp.factory(this));
|
||||
Recorder.showInspectorNoReply(this, RecorderApp.factory(this));
|
||||
|
||||
this._debugger.on(Debugger.Events.PausedStateChanged, () => {
|
||||
if (this._debugger.isPaused())
|
||||
Recorder.showInspector(this, RecorderApp.factory(this));
|
||||
Recorder.showInspectorNoReply(this, RecorderApp.factory(this));
|
||||
});
|
||||
|
||||
if (debugMode() === 'console')
|
||||
|
|
@ -525,7 +525,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
const internalMetadata = serverSideCallMetadata();
|
||||
const page = await this.newPage(internalMetadata);
|
||||
await page._setServerRequestInterceptor(handler => {
|
||||
handler.fulfill({ body: '<html></html>', requestUrl: handler.request().url() }).catch(() => {});
|
||||
handler.fulfill({ body: '<html></html>' }).catch(() => {});
|
||||
return true;
|
||||
});
|
||||
for (const origin of originsToSave) {
|
||||
|
|
@ -559,7 +559,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
isServerSide: false,
|
||||
});
|
||||
await page._setServerRequestInterceptor(handler => {
|
||||
handler.fulfill({ body: '<html></html>', requestUrl: handler.request().url() }).catch(() => {});
|
||||
handler.fulfill({ body: '<html></html>' }).catch(() => {});
|
||||
return true;
|
||||
});
|
||||
|
||||
|
|
@ -594,7 +594,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
const internalMetadata = serverSideCallMetadata();
|
||||
const page = await this.newPage(internalMetadata);
|
||||
await page._setServerRequestInterceptor(handler => {
|
||||
handler.fulfill({ body: '<html></html>', requestUrl: handler.request().url() }).catch(() => {});
|
||||
handler.fulfill({ body: '<html></html>' }).catch(() => {});
|
||||
return true;
|
||||
});
|
||||
for (const originState of state.origins) {
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ export const chromiumSwitches = [
|
|||
// Translate - https://github.com/microsoft/playwright/issues/16126
|
||||
// HttpsUpgrades - https://github.com/microsoft/playwright/pull/27605
|
||||
// PaintHolding - https://github.com/microsoft/playwright/issues/28023
|
||||
// PlzDedicatedWorker - https://github.com/microsoft/playwright/issues/31747
|
||||
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,PlzDedicatedWorker',
|
||||
// ThirdPartyStoragePartitioning - https://github.com/microsoft/playwright/issues/32230
|
||||
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,ThirdPartyStoragePartitioning',
|
||||
'--allow-pre-commit-input',
|
||||
'--disable-hang-monitor',
|
||||
'--disable-ipc-flooding-protection',
|
||||
|
|
|
|||
|
|
@ -840,7 +840,7 @@ CORS RFC1918 enforcement.
|
|||
resourceIPAddressSpace?: Network.IPAddressSpace;
|
||||
clientSecurityState?: Network.ClientSecurityState;
|
||||
}
|
||||
export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader";
|
||||
export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader"|"NavigationRegistrationUniqueScopeAlreadySet";
|
||||
export type SharedDictionaryError = "UseErrorCrossOriginNoCorsRequest"|"UseErrorDictionaryLoadFailure"|"UseErrorMatchingDictionaryNotUsed"|"UseErrorUnexpectedContentDictionaryHeader"|"WriteErrorCossOriginNoCorsRequest"|"WriteErrorDisallowedBySettings"|"WriteErrorExpiredResponse"|"WriteErrorFeatureDisabled"|"WriteErrorInsufficientResources"|"WriteErrorInvalidMatchField"|"WriteErrorInvalidStructuredHeader"|"WriteErrorNavigationRequest"|"WriteErrorNoMatchField"|"WriteErrorNonListMatchDestField"|"WriteErrorNonSecureContext"|"WriteErrorNonStringIdField"|"WriteErrorNonStringInMatchDestList"|"WriteErrorNonStringMatchField"|"WriteErrorNonTokenTypeField"|"WriteErrorRequestAborted"|"WriteErrorShuttingDown"|"WriteErrorTooLongIdField"|"WriteErrorUnsupportedType";
|
||||
/**
|
||||
* Details for issues around "Attribution Reporting API" usage.
|
||||
|
|
@ -1534,7 +1534,7 @@ events afterwards if enabled and recording.
|
|||
*/
|
||||
windowState?: WindowState;
|
||||
}
|
||||
export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"windowManagement";
|
||||
export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"windowManagement";
|
||||
export type PermissionSetting = "granted"|"denied"|"prompt";
|
||||
/**
|
||||
* Definition of PermissionDescriptor defined in the Permissions API:
|
||||
|
|
@ -3561,7 +3561,7 @@ front-end.
|
|||
/**
|
||||
* Pseudo element type.
|
||||
*/
|
||||
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new";
|
||||
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"select-fallback-button"|"select-fallback-button-text"|"picker";
|
||||
/**
|
||||
* Shadow root type.
|
||||
*/
|
||||
|
|
@ -3710,6 +3710,7 @@ The property is always undefined now.
|
|||
isSVG?: boolean;
|
||||
compatibilityMode?: CompatibilityMode;
|
||||
assignedSlot?: BackendNode;
|
||||
isScrollable?: boolean;
|
||||
}
|
||||
/**
|
||||
* A structure to hold the top-level node of a detached tree and an array of its retained descendants.
|
||||
|
|
@ -3954,6 +3955,19 @@ The property is always undefined now.
|
|||
* Called when top layer elements are changed.
|
||||
*/
|
||||
export type topLayerElementsUpdatedPayload = void;
|
||||
/**
|
||||
* Fired when a node's scrollability state changes.
|
||||
*/
|
||||
export type scrollableFlagUpdatedPayload = {
|
||||
/**
|
||||
* The id of the node.
|
||||
*/
|
||||
nodeId: DOM.NodeId;
|
||||
/**
|
||||
* If the node is scrollable.
|
||||
*/
|
||||
isScrollable: boolean;
|
||||
}
|
||||
/**
|
||||
* Called when a pseudo element is removed from an element.
|
||||
*/
|
||||
|
|
@ -8102,8 +8116,25 @@ or hexadecimal (0x prefixed) string.
|
|||
*/
|
||||
size: number;
|
||||
}
|
||||
/**
|
||||
* DOM object counter data.
|
||||
*/
|
||||
export interface DOMCounter {
|
||||
/**
|
||||
* Object name. Note: object names should be presumed volatile and clients should not expect
|
||||
the returned names to be consistent across runs.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Object count.
|
||||
*/
|
||||
count: number;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retruns current DOM object counters.
|
||||
*/
|
||||
export type getDOMCountersParameters = {
|
||||
}
|
||||
export type getDOMCountersReturnValue = {
|
||||
|
|
@ -8111,6 +8142,21 @@ or hexadecimal (0x prefixed) string.
|
|||
nodes: number;
|
||||
jsEventListeners: number;
|
||||
}
|
||||
/**
|
||||
* Retruns DOM object counters after preparing renderer for leak detection.
|
||||
*/
|
||||
export type getDOMCountersForLeakDetectionParameters = {
|
||||
}
|
||||
export type getDOMCountersForLeakDetectionReturnValue = {
|
||||
/**
|
||||
* DOM object counters.
|
||||
*/
|
||||
counters: DOMCounter[];
|
||||
}
|
||||
/**
|
||||
* Prepares for leak detection by terminating workers, stopping spellcheckers,
|
||||
dropping non-essential internal caches, running garbage collections, etc.
|
||||
*/
|
||||
export type prepareForLeakDetectionParameters = {
|
||||
}
|
||||
export type prepareForLeakDetectionReturnValue = {
|
||||
|
|
@ -8902,7 +8948,7 @@ This is a temporary ability and it will be removed in the future.
|
|||
/**
|
||||
* Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request.
|
||||
*/
|
||||
export type CookieExemptionReason = "None"|"UserSetting"|"TPCDMetadata"|"TPCDDeprecationTrial"|"TPCDHeuristics"|"EnterprisePolicy"|"StorageAccess"|"TopLevelStorageAccess"|"CorsOptIn"|"Scheme";
|
||||
export type CookieExemptionReason = "None"|"UserSetting"|"TPCDMetadata"|"TPCDDeprecationTrial"|"TopLevelTPCDDeprecationTrial"|"TPCDHeuristics"|"EnterprisePolicy"|"StorageAccess"|"TopLevelStorageAccess"|"Scheme";
|
||||
/**
|
||||
* A cookie which was not stored from a response with the corresponding reason.
|
||||
*/
|
||||
|
|
@ -11452,7 +11498,7 @@ as an ad.
|
|||
* All Permissions Policy features. This enum should match the one defined
|
||||
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
|
||||
*/
|
||||
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
|
||||
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
|
||||
/**
|
||||
* Reason for a permissions policy feature to be disabled.
|
||||
*/
|
||||
|
|
@ -12040,7 +12086,7 @@ https://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-expl
|
|||
/**
|
||||
* List of not restored reasons for back-forward cache.
|
||||
*/
|
||||
export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient";
|
||||
export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"ContentDiscarded"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient";
|
||||
/**
|
||||
* Types of not restored reasons for back-forward cache.
|
||||
*/
|
||||
|
|
@ -12151,6 +12197,16 @@ dependent on the reason:
|
|||
frameId: FrameId;
|
||||
reason: "remove"|"swap";
|
||||
}
|
||||
/**
|
||||
* Fired before frame subtree is detached. Emitted before any frame of the
|
||||
subtree is actually detached.
|
||||
*/
|
||||
export type frameSubtreeWillBeDetachedPayload = {
|
||||
/**
|
||||
* Id of the frame that is the root of the subtree that will be detached.
|
||||
*/
|
||||
frameId: FrameId;
|
||||
}
|
||||
/**
|
||||
* Fired once navigation of the frame has completed. Frame is now associated with the new loader.
|
||||
*/
|
||||
|
|
@ -14250,6 +14306,15 @@ int, only present for source registrations
|
|||
debugData: AttributionReportingAggregatableDebugReportingData[];
|
||||
aggregationCoordinatorOrigin?: string;
|
||||
}
|
||||
export interface AttributionScopesData {
|
||||
values: string[];
|
||||
/**
|
||||
* number instead of integer because not all uint32 can be represented by
|
||||
int
|
||||
*/
|
||||
limit: number;
|
||||
maxEventStates: number;
|
||||
}
|
||||
export interface AttributionReportingSourceRegistration {
|
||||
time: Network.TimeSinceEpoch;
|
||||
/**
|
||||
|
|
@ -14273,8 +14338,9 @@ int, only present for source registrations
|
|||
triggerDataMatching: AttributionReportingTriggerDataMatching;
|
||||
destinationLimitPriority: SignedInt64AsBase10;
|
||||
aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig;
|
||||
scopesData?: AttributionScopesData;
|
||||
}
|
||||
export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"destinationPerDayReportingLimitReached";
|
||||
export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxScopesChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"exceedsMaxEventStatesLimit"|"destinationPerDayReportingLimitReached";
|
||||
export type AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude";
|
||||
export interface AttributionReportingAggregatableValueDictEntry {
|
||||
key: string;
|
||||
|
|
@ -14317,6 +14383,7 @@ int
|
|||
sourceRegistrationTimeConfig: AttributionReportingSourceRegistrationTimeConfig;
|
||||
triggerContextId?: string;
|
||||
aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig;
|
||||
scopes: string[];
|
||||
}
|
||||
export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData";
|
||||
export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports";
|
||||
|
|
@ -17019,7 +17086,7 @@ status is shared by prefetchStatusUpdated and prerenderStatusUpdated.
|
|||
* TODO(https://crbug.com/1384419): revisit the list of PrefetchStatus and
|
||||
filter out the ones that aren't necessary to the developers.
|
||||
*/
|
||||
export type PrefetchStatus = "PrefetchAllowed"|"PrefetchFailedIneligibleRedirect"|"PrefetchFailedInvalidRedirect"|"PrefetchFailedMIMENotSupported"|"PrefetchFailedNetError"|"PrefetchFailedNon2XX"|"PrefetchFailedPerPageLimitExceeded"|"PrefetchEvictedAfterCandidateRemoved"|"PrefetchEvictedForNewerPrefetch"|"PrefetchHeldback"|"PrefetchIneligibleRetryAfter"|"PrefetchIsPrivacyDecoy"|"PrefetchIsStale"|"PrefetchNotEligibleBrowserContextOffTheRecord"|"PrefetchNotEligibleDataSaverEnabled"|"PrefetchNotEligibleExistingProxy"|"PrefetchNotEligibleHostIsNonUnique"|"PrefetchNotEligibleNonDefaultStoragePartition"|"PrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy"|"PrefetchNotEligibleSchemeIsNotHttps"|"PrefetchNotEligibleUserHasCookies"|"PrefetchNotEligibleUserHasServiceWorker"|"PrefetchNotEligibleBatterySaverEnabled"|"PrefetchNotEligiblePreloadingDisabled"|"PrefetchNotFinishedInTime"|"PrefetchNotStarted"|"PrefetchNotUsedCookiesChanged"|"PrefetchProxyNotAvailable"|"PrefetchResponseUsed"|"PrefetchSuccessfulButNotUsed"|"PrefetchNotUsedProbeFailed";
|
||||
export type PrefetchStatus = "PrefetchAllowed"|"PrefetchFailedIneligibleRedirect"|"PrefetchFailedInvalidRedirect"|"PrefetchFailedMIMENotSupported"|"PrefetchFailedNetError"|"PrefetchFailedNon2XX"|"PrefetchEvictedAfterCandidateRemoved"|"PrefetchEvictedForNewerPrefetch"|"PrefetchHeldback"|"PrefetchIneligibleRetryAfter"|"PrefetchIsPrivacyDecoy"|"PrefetchIsStale"|"PrefetchNotEligibleBrowserContextOffTheRecord"|"PrefetchNotEligibleDataSaverEnabled"|"PrefetchNotEligibleExistingProxy"|"PrefetchNotEligibleHostIsNonUnique"|"PrefetchNotEligibleNonDefaultStoragePartition"|"PrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy"|"PrefetchNotEligibleSchemeIsNotHttps"|"PrefetchNotEligibleUserHasCookies"|"PrefetchNotEligibleUserHasServiceWorker"|"PrefetchNotEligibleBatterySaverEnabled"|"PrefetchNotEligiblePreloadingDisabled"|"PrefetchNotFinishedInTime"|"PrefetchNotStarted"|"PrefetchNotUsedCookiesChanged"|"PrefetchProxyNotAvailable"|"PrefetchResponseUsed"|"PrefetchSuccessfulButNotUsed"|"PrefetchNotUsedProbeFailed";
|
||||
/**
|
||||
* Information of headers to be displayed when the header mismatch occurred.
|
||||
*/
|
||||
|
|
@ -20115,6 +20182,7 @@ Error was thrown.
|
|||
"DOM.inlineStyleInvalidated": DOM.inlineStyleInvalidatedPayload;
|
||||
"DOM.pseudoElementAdded": DOM.pseudoElementAddedPayload;
|
||||
"DOM.topLayerElementsUpdated": DOM.topLayerElementsUpdatedPayload;
|
||||
"DOM.scrollableFlagUpdated": DOM.scrollableFlagUpdatedPayload;
|
||||
"DOM.pseudoElementRemoved": DOM.pseudoElementRemovedPayload;
|
||||
"DOM.setChildNodes": DOM.setChildNodesPayload;
|
||||
"DOM.shadowRootPopped": DOM.shadowRootPoppedPayload;
|
||||
|
|
@ -20173,6 +20241,7 @@ Error was thrown.
|
|||
"Page.frameAttached": Page.frameAttachedPayload;
|
||||
"Page.frameClearedScheduledNavigation": Page.frameClearedScheduledNavigationPayload;
|
||||
"Page.frameDetached": Page.frameDetachedPayload;
|
||||
"Page.frameSubtreeWillBeDetached": Page.frameSubtreeWillBeDetachedPayload;
|
||||
"Page.frameNavigated": Page.frameNavigatedPayload;
|
||||
"Page.documentOpened": Page.documentOpenedPayload;
|
||||
"Page.frameResized": Page.frameResizedPayload;
|
||||
|
|
@ -20539,6 +20608,7 @@ Error was thrown.
|
|||
"Log.startViolationsReport": Log.startViolationsReportParameters;
|
||||
"Log.stopViolationsReport": Log.stopViolationsReportParameters;
|
||||
"Memory.getDOMCounters": Memory.getDOMCountersParameters;
|
||||
"Memory.getDOMCountersForLeakDetection": Memory.getDOMCountersForLeakDetectionParameters;
|
||||
"Memory.prepareForLeakDetection": Memory.prepareForLeakDetectionParameters;
|
||||
"Memory.forciblyPurgeJavaScriptMemory": Memory.forciblyPurgeJavaScriptMemoryParameters;
|
||||
"Memory.setPressureNotificationsSuppressed": Memory.setPressureNotificationsSuppressedParameters;
|
||||
|
|
@ -21148,6 +21218,7 @@ Error was thrown.
|
|||
"Log.startViolationsReport": Log.startViolationsReportReturnValue;
|
||||
"Log.stopViolationsReport": Log.stopViolationsReportReturnValue;
|
||||
"Memory.getDOMCounters": Memory.getDOMCountersReturnValue;
|
||||
"Memory.getDOMCountersForLeakDetection": Memory.getDOMCountersForLeakDetectionReturnValue;
|
||||
"Memory.prepareForLeakDetection": Memory.prepareForLeakDetectionReturnValue;
|
||||
"Memory.forciblyPurgeJavaScriptMemory": Memory.forciblyPurgeJavaScriptMemoryReturnValue;
|
||||
"Memory.setPressureNotificationsSuppressed": Memory.setPressureNotificationsSuppressedReturnValue;
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
return formatter.format();
|
||||
}
|
||||
|
||||
const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.ContentFrame()`);
|
||||
const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.ContentFrame`);
|
||||
const subject = `${pageAlias}${locators.join('')}`;
|
||||
const signals = toSignalMap(action);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import type * as types from '../types';
|
|||
import type { ActionInContext, LanguageGenerator, LanguageGeneratorOptions } from './types';
|
||||
|
||||
export function generateCode(actions: ActionInContext[], languageGenerator: LanguageGenerator, options: LanguageGeneratorOptions) {
|
||||
actions = collapseActions(actions);
|
||||
const header = languageGenerator.generateHeader(options);
|
||||
const footer = languageGenerator.generateFooter(options.saveStorage);
|
||||
const actionTexts = actions.map(a => languageGenerator.generateAction(a)).filter(Boolean);
|
||||
|
|
@ -70,6 +69,23 @@ export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModif
|
|||
return result;
|
||||
}
|
||||
|
||||
export function fromKeyboardModifiers(modifiers?: types.SmartKeyboardModifier[]): number {
|
||||
let result = 0;
|
||||
if (!modifiers)
|
||||
return result;
|
||||
if (modifiers.includes('Alt'))
|
||||
result |= 1;
|
||||
if (modifiers.includes('Control'))
|
||||
result |= 2;
|
||||
if (modifiers.includes('ControlOrMeta'))
|
||||
result |= 2;
|
||||
if (modifiers.includes('Meta'))
|
||||
result |= 4;
|
||||
if (modifiers.includes('Shift'))
|
||||
result |= 8;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function toClickOptionsForSourceCode(action: actions.ClickAction): types.MouseClickOptions {
|
||||
const modifiers = toKeyboardModifiers(action.modifiers);
|
||||
const options: types.MouseClickOptions = {};
|
||||
|
|
@ -84,19 +100,3 @@ export function toClickOptionsForSourceCode(action: actions.ClickAction): types.
|
|||
options.position = action.position;
|
||||
return options;
|
||||
}
|
||||
|
||||
function collapseActions(actions: ActionInContext[]): ActionInContext[] {
|
||||
const result: ActionInContext[] = [];
|
||||
for (const action of actions) {
|
||||
const lastAction = result[result.length - 1];
|
||||
const isSameAction = lastAction && lastAction.action.name === action.action.name && lastAction.frame.pageAlias === action.frame.pageAlias && lastAction.frame.framePath.join('|') === action.frame.framePath.join('|');
|
||||
const isSameSelector = lastAction && 'selector' in lastAction.action && 'selector' in action.action && action.action.selector === lastAction.action.selector;
|
||||
const shouldMerge = isSameAction && (action.action.name === 'navigate' || (action.action.name === 'fill' && isSameSelector));
|
||||
if (!shouldMerge) {
|
||||
result.push(action);
|
||||
continue;
|
||||
}
|
||||
result[result.length - 1] = action;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
return formatter.format();
|
||||
}
|
||||
|
||||
const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.content_frame()`);
|
||||
const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.content_frame`);
|
||||
const subject = `${pageAlias}${locators.join('')}`;
|
||||
const signals = toSignalMap(action);
|
||||
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ export class DebugController extends SdkObject {
|
|||
const contexts = new Set<BrowserContext>();
|
||||
for (const page of this._playwright.allPages())
|
||||
contexts.add(page.context());
|
||||
const result = await Promise.all([...contexts].map(c => Recorder.show(c, () => Promise.resolve(new InspectingRecorderApp(this)), { omitCallTracking: true })));
|
||||
const result = await Promise.all([...contexts].map(c => Recorder.showInspector(c, { omitCallTracking: true }, () => Promise.resolve(new InspectingRecorderApp(this)))));
|
||||
return result.filter(Boolean) as Recorder[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36 Edge/14.14263",
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36 Edge/14.14263",
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36 Edge/14.14263",
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36 Edge/14.14263",
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"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/129.0.6668.42 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Safari/537.36",
|
||||
"screen": {
|
||||
"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/129.0.6668.42 Safari/537.36 Edg/129.0.6668.42",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Safari/537.36 Edg/130.0.6723.6",
|
||||
"screen": {
|
||||
"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/129.0.6668.42 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Safari/537.36",
|
||||
"screen": {
|
||||
"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/129.0.6668.42 Safari/537.36 Edg/129.0.6668.42",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.6 Safari/537.36 Edg/130.0.6723.6",
|
||||
"screen": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export class Dialog extends SdkObject {
|
|||
this._onHandle = onHandle;
|
||||
this._defaultValue = defaultValue || '';
|
||||
this._page._frameManager.dialogDidOpen(this);
|
||||
this.instrumentation.onDialog(this);
|
||||
}
|
||||
|
||||
page() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
[*]
|
||||
../../common/
|
||||
../../generated/
|
||||
../../protocol/
|
||||
../../utils/
|
||||
../../zipBundle.ts
|
||||
|
|
|
|||
|
|
@ -41,12 +41,14 @@ import { serializeError } from '../errors';
|
|||
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
|
||||
import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer';
|
||||
import { RecorderApp } from '../recorder/recorderApp';
|
||||
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
|
||||
|
||||
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
|
||||
_type_EventTarget = true;
|
||||
_type_BrowserContext = true;
|
||||
private _context: BrowserContext;
|
||||
private _subscriptions = new Set<channels.BrowserContextUpdateSubscriptionParams['event']>();
|
||||
_webSocketInterceptionPatterns: channels.BrowserContextSetWebSocketInterceptionPatternsParams['patterns'] = [];
|
||||
|
||||
constructor(parentScope: DispatcherScope, context: BrowserContext) {
|
||||
// We will reparent these to the context below.
|
||||
|
|
@ -283,6 +285,12 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
|||
});
|
||||
}
|
||||
|
||||
async setWebSocketInterceptionPatterns(params: channels.PageSetWebSocketInterceptionPatternsParams, metadata: CallMetadata): Promise<void> {
|
||||
this._webSocketInterceptionPatterns = params.patterns;
|
||||
if (params.patterns.length)
|
||||
await WebSocketRouteDispatcher.installIfNeeded(this, this._context);
|
||||
}
|
||||
|
||||
async storageState(params: channels.BrowserContextStorageStateParams, metadata: CallMetadata): Promise<channels.BrowserContextStorageStateResult> {
|
||||
return await this._context.storageState();
|
||||
}
|
||||
|
|
@ -292,9 +300,18 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
|||
await this._context.close(params);
|
||||
}
|
||||
|
||||
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
|
||||
const factory = process.env.PW_RECORDER_IS_TRACE_VIEWER ? RecorderInTraceViewer.factory(this._context) : RecorderApp.factory(this._context);
|
||||
await Recorder.show(this._context, factory, params);
|
||||
async enableRecorder(params: channels.BrowserContextEnableRecorderParams): Promise<void> {
|
||||
if (params.codegenMode === 'trace-events') {
|
||||
await this._context.tracing.start({
|
||||
name: 'trace',
|
||||
snapshots: true,
|
||||
screenshots: true,
|
||||
live: true,
|
||||
});
|
||||
await Recorder.show('trace-events', this._context, RecorderInTraceViewer.factory(this._context), params);
|
||||
} else {
|
||||
await Recorder.show('actions', this._context, RecorderApp.factory(this._context), params);
|
||||
}
|
||||
}
|
||||
|
||||
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {
|
||||
|
|
|
|||
|
|
@ -35,12 +35,14 @@ import { ArtifactDispatcher } from './artifactDispatcher';
|
|||
import type { Download } from '../download';
|
||||
import { createGuid, urlMatches } from '../../utils';
|
||||
import type { BrowserContextDispatcher } from './browserContextDispatcher';
|
||||
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
|
||||
|
||||
export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, BrowserContextDispatcher> implements channels.PageChannel {
|
||||
_type_EventTarget = true;
|
||||
_type_Page = true;
|
||||
private _page: Page;
|
||||
_subscriptions = new Set<channels.PageUpdateSubscriptionParams['event']>();
|
||||
_webSocketInterceptionPatterns: channels.PageSetWebSocketInterceptionPatternsParams['patterns'] = [];
|
||||
|
||||
static from(parentScope: BrowserContextDispatcher, page: Page): PageDispatcher {
|
||||
return PageDispatcher.fromNullable(parentScope, page)!;
|
||||
|
|
@ -186,6 +188,12 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
|
|||
});
|
||||
}
|
||||
|
||||
async setWebSocketInterceptionPatterns(params: channels.PageSetWebSocketInterceptionPatternsParams, metadata: CallMetadata): Promise<void> {
|
||||
this._webSocketInterceptionPatterns = params.patterns;
|
||||
if (params.patterns.length)
|
||||
await WebSocketRouteDispatcher.installIfNeeded(this.parentScope(), this._page);
|
||||
}
|
||||
|
||||
async expectScreenshot(params: channels.PageExpectScreenshotParams, metadata: CallMetadata): Promise<channels.PageExpectScreenshotResult> {
|
||||
const mask: { frame: Frame, selector: string }[] = (params.mask || []).map(({ frame, selector }) => ({
|
||||
frame: (frame as FrameDispatcher)._object,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { BrowserContext } from '../browserContext';
|
||||
import type { Frame } from '../frames';
|
||||
import { Page } from '../page';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import { Dispatcher } from './dispatcher';
|
||||
import { createGuid, urlMatches } from '../../utils';
|
||||
import { PageDispatcher } from './pageDispatcher';
|
||||
import type { BrowserContextDispatcher } from './browserContextDispatcher';
|
||||
import * as webSocketMockSource from '../../generated/webSocketMockSource';
|
||||
import type * as ws from '../injected/webSocketMock';
|
||||
import { eventsHelper } from '../../utils/eventsHelper';
|
||||
|
||||
const kBindingInstalledSymbol = Symbol('webSocketRouteBindingInstalled');
|
||||
const kInitScriptInstalledSymbol = Symbol('webSocketRouteInitScriptInstalled');
|
||||
|
||||
export class WebSocketRouteDispatcher extends Dispatcher<{ guid: string }, channels.WebSocketRouteChannel, PageDispatcher | BrowserContextDispatcher> implements channels.WebSocketRouteChannel {
|
||||
_type_WebSocketRoute = true;
|
||||
private _id: string;
|
||||
private _frame: Frame;
|
||||
private static _idToDispatcher = new Map<string, WebSocketRouteDispatcher>();
|
||||
|
||||
constructor(scope: PageDispatcher | BrowserContextDispatcher, id: string, url: string, frame: Frame) {
|
||||
super(scope, { guid: 'webSocketRoute@' + createGuid() }, 'WebSocketRoute', { url });
|
||||
this._id = id;
|
||||
this._frame = frame;
|
||||
this._eventListeners.push(
|
||||
// When the frame navigates or detaches, there will be no more communication
|
||||
// from the mock websocket, so pretend like it was closed.
|
||||
eventsHelper.addEventListener(frame._page, Page.Events.InternalFrameNavigatedToNewDocument, (frame: Frame) => {
|
||||
if (frame === this._frame)
|
||||
this._onClose();
|
||||
}),
|
||||
eventsHelper.addEventListener(frame._page, Page.Events.FrameDetached, (frame: Frame) => {
|
||||
if (frame === this._frame)
|
||||
this._onClose();
|
||||
}),
|
||||
eventsHelper.addEventListener(frame._page, Page.Events.Close, () => this._onClose()),
|
||||
eventsHelper.addEventListener(frame._page, Page.Events.Crash, () => this._onClose()),
|
||||
);
|
||||
WebSocketRouteDispatcher._idToDispatcher.set(this._id, this);
|
||||
(scope as any)._dispatchEvent('webSocketRoute', { webSocketRoute: this });
|
||||
}
|
||||
|
||||
static async installIfNeeded(contextDispatcher: BrowserContextDispatcher, target: Page | BrowserContext) {
|
||||
const context = target instanceof Page ? target.context() : target;
|
||||
if (!(context as any)[kBindingInstalledSymbol]) {
|
||||
(context as any)[kBindingInstalledSymbol] = true;
|
||||
|
||||
await context.exposeBinding('__pwWebSocketBinding', false, (source, payload: ws.BindingPayload) => {
|
||||
if (payload.type === 'onCreate') {
|
||||
const pageDispatcher = PageDispatcher.fromNullable(contextDispatcher, source.page);
|
||||
let scope: PageDispatcher | BrowserContextDispatcher | undefined;
|
||||
if (pageDispatcher && matchesPattern(pageDispatcher, context._options.baseURL, payload.url))
|
||||
scope = pageDispatcher;
|
||||
else if (matchesPattern(contextDispatcher, context._options.baseURL, payload.url))
|
||||
scope = contextDispatcher;
|
||||
if (scope) {
|
||||
new WebSocketRouteDispatcher(scope, payload.id, payload.url, source.frame);
|
||||
} else {
|
||||
const request: ws.PassthroughRequest = { id: payload.id, type: 'passthrough' };
|
||||
source.frame.evaluateExpression(`globalThis.__pwWebSocketDispatch(${JSON.stringify(request)})`).catch(() => {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const dispatcher = WebSocketRouteDispatcher._idToDispatcher.get(payload.id);
|
||||
if (payload.type === 'onMessageFromPage')
|
||||
dispatcher?._dispatchEvent('messageFromPage', { message: payload.data.data, isBase64: payload.data.isBase64 });
|
||||
if (payload.type === 'onMessageFromServer')
|
||||
dispatcher?._dispatchEvent('messageFromServer', { message: payload.data.data, isBase64: payload.data.isBase64 });
|
||||
if (payload.type === 'onClose')
|
||||
dispatcher?._onClose();
|
||||
});
|
||||
}
|
||||
|
||||
if (!(target as any)[kInitScriptInstalledSymbol]) {
|
||||
(target as any)[kInitScriptInstalledSymbol] = true;
|
||||
await target.addInitScript(`
|
||||
(() => {
|
||||
const module = {};
|
||||
${webSocketMockSource.source}
|
||||
(module.exports.inject())(globalThis);
|
||||
})();
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
async connect(params: channels.WebSocketRouteConnectParams) {
|
||||
await this._evaluateAPIRequest({ id: this._id, type: 'connect' });
|
||||
}
|
||||
|
||||
async ensureOpened(params: channels.WebSocketRouteEnsureOpenedParams) {
|
||||
await this._evaluateAPIRequest({ id: this._id, type: 'ensureOpened' });
|
||||
}
|
||||
|
||||
async sendToPage(params: channels.WebSocketRouteSendToPageParams) {
|
||||
await this._evaluateAPIRequest({ id: this._id, type: 'sendToPage', data: { data: params.message, isBase64: params.isBase64 } });
|
||||
}
|
||||
|
||||
async sendToServer(params: channels.WebSocketRouteSendToServerParams) {
|
||||
await this._evaluateAPIRequest({ id: this._id, type: 'sendToServer', data: { data: params.message, isBase64: params.isBase64 } });
|
||||
}
|
||||
|
||||
async close(params: channels.WebSocketRouteCloseParams) {
|
||||
await this._evaluateAPIRequest({ id: this._id, type: 'close', code: params.code, reason: params.reason, wasClean: true });
|
||||
}
|
||||
|
||||
private async _evaluateAPIRequest(request: ws.APIRequest) {
|
||||
await this._frame.evaluateExpression(`globalThis.__pwWebSocketDispatch(${JSON.stringify(request)})`).catch(() => {});
|
||||
}
|
||||
|
||||
override _onDispose() {
|
||||
WebSocketRouteDispatcher._idToDispatcher.delete(this._id);
|
||||
}
|
||||
|
||||
_onClose() {
|
||||
// We could enter here twice upon page closure:
|
||||
// - first from the recursive dispose inintiated by PageDispatcher;
|
||||
// - then from our own page.on('close') listener.
|
||||
if (this._disposed)
|
||||
return;
|
||||
this._dispatchEvent('close');
|
||||
this._dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function matchesPattern(dispatcher: PageDispatcher | BrowserContextDispatcher, baseURL: string | undefined, url: string) {
|
||||
for (const pattern of dispatcher._webSocketInterceptionPatterns || []) {
|
||||
const urlMatch = pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags) : pattern.glob;
|
||||
if (urlMatches(baseURL, url, urlMatch))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -35,16 +35,25 @@ export class Download {
|
|||
this._suggestedFilename = suggestedFilename;
|
||||
page._browserContext._downloads.add(this);
|
||||
if (suggestedFilename !== undefined)
|
||||
this._page.emit(Page.Events.Download, this);
|
||||
this._fireDownloadEvent();
|
||||
}
|
||||
|
||||
page(): Page {
|
||||
return this._page;
|
||||
}
|
||||
|
||||
_filenameSuggested(suggestedFilename: string) {
|
||||
assert(this._suggestedFilename === undefined);
|
||||
this._suggestedFilename = suggestedFilename;
|
||||
this._page.emit(Page.Events.Download, this);
|
||||
this._fireDownloadEvent();
|
||||
}
|
||||
|
||||
suggestedFilename(): string {
|
||||
return this._suggestedFilename!;
|
||||
}
|
||||
|
||||
private _fireDownloadEvent() {
|
||||
this._page.instrumentation.onDownload(this._page, this);
|
||||
this._page.emit(Page.Events.Download, this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import type * as types from './types';
|
|||
import type { HeadersArray, ProxySettings } from './types';
|
||||
import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
|
||||
import type * as har from '@trace/har';
|
||||
import { TLSSocket } from 'tls';
|
||||
|
||||
type FetchRequestOptions = {
|
||||
userAgent: string;
|
||||
|
|
@ -73,6 +74,9 @@ export type APIRequestFinishedEvent = {
|
|||
statusMessage: string;
|
||||
body?: Buffer;
|
||||
timings: har.Timings;
|
||||
serverIPAddress?: string;
|
||||
serverPort?: number;
|
||||
securityDetails?: har.SecurityDetails;
|
||||
};
|
||||
|
||||
type SendRequestOptions = https.RequestOptions & {
|
||||
|
|
@ -259,6 +263,7 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
try {
|
||||
return await this._sendRequest(progress, url, options, postData);
|
||||
} catch (e) {
|
||||
e = rewriteOpenSSLErrorIfNeeded(e);
|
||||
if (maxRetries === 0)
|
||||
throw e;
|
||||
if (i === maxRetries || (options.deadline && monotonicTime() + backoff > options.deadline))
|
||||
|
|
@ -302,6 +307,10 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
let tcpConnectionAt: number | undefined;
|
||||
let tlsHandshakeAt: number | undefined;
|
||||
let requestFinishAt: number | undefined;
|
||||
let serverIPAddress: string | undefined;
|
||||
let serverPort: number | undefined;
|
||||
|
||||
let securityDetails: har.SecurityDetails | undefined;
|
||||
|
||||
const request = requestConstructor(url, requestOptions as any, async response => {
|
||||
const responseAt = monotonicTime();
|
||||
|
|
@ -328,6 +337,9 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
cookies,
|
||||
body,
|
||||
timings,
|
||||
serverIPAddress,
|
||||
serverPort,
|
||||
securityDetails,
|
||||
};
|
||||
this.emit(APIRequestContext.Events.RequestFinished, requestFinishedEvent);
|
||||
};
|
||||
|
|
@ -464,7 +476,7 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
body.on('data', chunk => chunks.push(chunk));
|
||||
body.on('end', notifyBodyFinished);
|
||||
});
|
||||
request.on('error', error => reject(rewriteOpenSSLErrorIfNeeded(error)));
|
||||
request.on('error', reject);
|
||||
|
||||
const disposeListener = () => {
|
||||
reject(new Error('Request context disposed.'));
|
||||
|
|
@ -482,7 +494,23 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
// non-happy-eyeballs sockets
|
||||
socket.on('lookup', () => { dnsLookupAt = monotonicTime(); });
|
||||
socket.on('connect', () => { tcpConnectionAt = monotonicTime(); });
|
||||
socket.on('secureConnect', () => { tlsHandshakeAt = monotonicTime(); });
|
||||
socket.on('secureConnect', () => {
|
||||
tlsHandshakeAt = monotonicTime();
|
||||
|
||||
if (socket instanceof TLSSocket) {
|
||||
const peerCertificate = socket.getPeerCertificate();
|
||||
securityDetails = {
|
||||
protocol: socket.getProtocol() ?? undefined,
|
||||
subjectName: peerCertificate.subject.CN,
|
||||
validFrom: new Date(peerCertificate.valid_from).getTime() / 1000,
|
||||
validTo: new Date(peerCertificate.valid_to).getTime() / 1000,
|
||||
issuer: peerCertificate.issuer.CN
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
serverIPAddress = socket.remoteAddress;
|
||||
serverPort = socket.remotePort;
|
||||
});
|
||||
request.on('finish', () => { requestFinishAt = monotonicTime(); });
|
||||
|
||||
|
|
|
|||
|
|
@ -782,13 +782,16 @@ export class Frame extends SdkObject {
|
|||
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
|
||||
return controller.run(async progress => {
|
||||
progress.log(`waiting for ${this._asLocator(selector)}${state === 'attached' ? '' : ' to be ' + state}`);
|
||||
return await this.waitForSelectorInternal(progress, selector, options, scope);
|
||||
return await this.waitForSelectorInternal(progress, selector, true, options, scope);
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
async waitForSelectorInternal(progress: Progress, selector: string, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
|
||||
async waitForSelectorInternal(progress: Progress, selector: string, performLocatorHandlersCheckpoint: boolean, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
|
||||
const { state = 'visible' } = options;
|
||||
const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
|
||||
if (performLocatorHandlersCheckpoint)
|
||||
await this._page.performLocatorHandlersCheckpoint(progress);
|
||||
|
||||
const resolved = await this.selectors.resolveInjectedForSelector(selector, options, scope);
|
||||
progress.throwIfAborted();
|
||||
if (!resolved) {
|
||||
|
|
|
|||
|
|
@ -213,11 +213,19 @@ export class HarTracer {
|
|||
harEntry.response.httpVersion = event.httpVersion;
|
||||
harEntry.response.redirectURL = event.headers.location || '';
|
||||
|
||||
if (!this._options.omitServerIP) {
|
||||
harEntry.serverIPAddress = event.serverIPAddress;
|
||||
harEntry._serverPort = event.serverPort;
|
||||
}
|
||||
|
||||
if (!this._options.omitTiming) {
|
||||
harEntry.timings = event.timings;
|
||||
this._computeHarEntryTotalTime(harEntry);
|
||||
}
|
||||
|
||||
if (!this._options.omitSecurityDetails)
|
||||
harEntry._securityDetails = event.securityDetails;
|
||||
|
||||
for (let i = 0; i < event.rawHeaders.length; i += 2) {
|
||||
harEntry.response.headers.push({
|
||||
name: event.rawHeaders[i],
|
||||
|
|
@ -236,6 +244,8 @@ export class HarTracer {
|
|||
if (contentType)
|
||||
content.mimeType = contentType;
|
||||
this._storeResponseContent(event.body, content, 'other');
|
||||
if (!this._options.omitSizes)
|
||||
harEntry.response.bodySize = event.body?.length ?? 0;
|
||||
|
||||
if (this._started)
|
||||
this._delegate.onEntryFinished(harEntry);
|
||||
|
|
|
|||
|
|
@ -1039,9 +1039,12 @@ export class Recorder {
|
|||
|
||||
this.highlight.install();
|
||||
// some frameworks erase the DOM on hydration, this ensures it's reattached
|
||||
const recreationInterval = setInterval(() => {
|
||||
let recreationInterval: number | undefined;
|
||||
const recreate = () => {
|
||||
this.highlight.install();
|
||||
}, 500);
|
||||
recreationInterval = this.injectedScript.builtinSetTimeout(recreate, 500);
|
||||
};
|
||||
recreationInterval = this.injectedScript.builtinSetTimeout(recreate, 500);
|
||||
this._listeners.push(() => clearInterval(recreationInterval));
|
||||
|
||||
this.overlay?.install();
|
||||
|
|
|
|||
343
packages/playwright-core/src/server/injected/webSocketMock.ts
Normal file
343
packages/playwright-core/src/server/injected/webSocketMock.ts
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type WebSocketMessage = string | ArrayBufferLike | Blob | ArrayBufferView;
|
||||
export type WSData = { data: string, isBase64: boolean };
|
||||
|
||||
export type OnCreatePayload = { type: 'onCreate', id: string, url: string };
|
||||
export type OnMessageFromPagePayload = { type: 'onMessageFromPage', id: string, data: WSData };
|
||||
export type OnClosePayload = { type: 'onClose', id: string, code: number | undefined, reason: string | undefined, wasClean: boolean };
|
||||
export type OnMessageFromServerPayload = { type: 'onMessageFromServer', id: string, data: WSData };
|
||||
export type BindingPayload = OnCreatePayload | OnMessageFromPagePayload | OnMessageFromServerPayload | OnClosePayload;
|
||||
|
||||
export type ConnectRequest = { type: 'connect', id: string };
|
||||
export type PassthroughRequest = { type: 'passthrough', id: string };
|
||||
export type EnsureOpenedRequest = { type: 'ensureOpened', id: string };
|
||||
export type SendToPageRequest = { type: 'sendToPage', id: string, data: WSData };
|
||||
export type SendToServerRequest = { type: 'sendToServer', id: string, data: WSData };
|
||||
export type CloseRequest = { type: 'close', id: string, code: number | undefined, reason: string | undefined, wasClean: boolean };
|
||||
export type APIRequest = ConnectRequest | PassthroughRequest | EnsureOpenedRequest | SendToPageRequest | SendToServerRequest | CloseRequest;
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
type GlobalThis = typeof globalThis;
|
||||
|
||||
export function inject(globalThis: GlobalThis) {
|
||||
if ((globalThis as any).__pwWebSocketDispatch)
|
||||
return;
|
||||
|
||||
function generateId() {
|
||||
const bytes = new Uint8Array(32);
|
||||
globalThis.crypto.getRandomValues(bytes);
|
||||
const hex = '0123456789abcdef';
|
||||
return [...bytes].map(value => {
|
||||
const high = Math.floor(value / 16);
|
||||
const low = value % 16;
|
||||
return hex[high] + hex[low];
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function bufferToData(b: Uint8Array): WSData {
|
||||
let s = '';
|
||||
for (let i = 0; i < b.length; i++)
|
||||
s += String.fromCharCode(b[i]);
|
||||
return { data: globalThis.btoa(s), isBase64: true };
|
||||
}
|
||||
|
||||
function stringToBuffer(s: string): ArrayBuffer {
|
||||
s = globalThis.atob(s);
|
||||
const b = new Uint8Array(s.length);
|
||||
for (let i = 0; i < s.length; i++)
|
||||
b[i] = s.charCodeAt(i);
|
||||
return b.buffer;
|
||||
}
|
||||
|
||||
// Note: this function tries to be synchronous when it can to preserve the ability to send
|
||||
// multiple messages synchronously in the same order and then synchronously close.
|
||||
function messageToData(message: WebSocketMessage, cb: (data: WSData) => any) {
|
||||
if (message instanceof globalThis.Blob)
|
||||
return message.arrayBuffer().then(buffer => cb(bufferToData(new Uint8Array(buffer))));
|
||||
if (typeof message === 'string')
|
||||
return cb({ data: message, isBase64: false });
|
||||
if (ArrayBuffer.isView(message))
|
||||
return cb(bufferToData(new Uint8Array(message.buffer, message.byteOffset, message.byteLength)));
|
||||
return cb(bufferToData(new Uint8Array(message)));
|
||||
}
|
||||
|
||||
function dataToMessage(data: WSData, binaryType: 'blob' | 'arraybuffer'): WebSocketMessage {
|
||||
if (!data.isBase64)
|
||||
return data.data;
|
||||
const buffer = stringToBuffer(data.data);
|
||||
return binaryType === 'arraybuffer' ? buffer : new Blob([buffer]);
|
||||
}
|
||||
|
||||
const binding = (globalThis as any).__pwWebSocketBinding as (message: BindingPayload) => void;
|
||||
const NativeWebSocket: typeof WebSocket = globalThis.WebSocket;
|
||||
const idToWebSocket = new Map<string, WebSocketMock>();
|
||||
(globalThis as any).__pwWebSocketDispatch = (request: APIRequest) => {
|
||||
const ws = idToWebSocket.get(request.id);
|
||||
if (!ws)
|
||||
return;
|
||||
if (request.type === 'connect')
|
||||
ws._apiConnect();
|
||||
if (request.type === 'passthrough')
|
||||
ws._apiPassThrough();
|
||||
if (request.type === 'ensureOpened')
|
||||
ws._apiEnsureOpened();
|
||||
if (request.type === 'sendToPage')
|
||||
ws._apiSendToPage(dataToMessage(request.data, ws.binaryType));
|
||||
if (request.type === 'close')
|
||||
ws._apiClose(request.code, request.reason, request.wasClean);
|
||||
if (request.type === 'sendToServer')
|
||||
ws._apiSendToServer(dataToMessage(request.data, ws.binaryType));
|
||||
};
|
||||
|
||||
class WebSocketMock extends EventTarget {
|
||||
static readonly CONNECTING: 0 = 0; // WebSocket.CONNECTING
|
||||
static readonly OPEN: 1 = 1; // WebSocket.OPEN
|
||||
static readonly CLOSING: 2 = 2; // WebSocket.CLOSING
|
||||
static readonly CLOSED: 3 = 3; // WebSocket.CLOSED
|
||||
|
||||
CONNECTING: 0 = 0; // WebSocket.CONNECTING
|
||||
OPEN: 1 = 1; // WebSocket.OPEN
|
||||
CLOSING: 2 = 2; // WebSocket.CLOSING
|
||||
CLOSED: 3 = 3; // WebSocket.CLOSED
|
||||
|
||||
private _oncloseListener: WebSocket['onclose'] = null;
|
||||
private _onerrorListener: WebSocket['onerror'] = null;
|
||||
private _onmessageListener: WebSocket['onmessage'] = null;
|
||||
private _onopenListener: WebSocket['onopen'] = null;
|
||||
|
||||
bufferedAmount: number = 0;
|
||||
extensions: string = '';
|
||||
protocol: string = '';
|
||||
readyState: number = 0;
|
||||
readonly url: string;
|
||||
|
||||
private _id: string;
|
||||
private _origin: string = '';
|
||||
private _protocols?: string | string[];
|
||||
private _ws?: WebSocket;
|
||||
private _passthrough = false;
|
||||
private _wsBufferedMessages: WebSocketMessage[] = [];
|
||||
private _binaryType: BinaryType = 'blob';
|
||||
|
||||
constructor(url: string | URL, protocols?: string | string[]) {
|
||||
super();
|
||||
|
||||
this.url = typeof url === 'string' ? url : url.href;
|
||||
try {
|
||||
this._origin = new URL(url).origin;
|
||||
} catch {
|
||||
}
|
||||
this._protocols = protocols;
|
||||
|
||||
this._id = generateId();
|
||||
idToWebSocket.set(this._id, this);
|
||||
binding({ type: 'onCreate', id: this._id, url: this.url });
|
||||
}
|
||||
|
||||
// --- native WebSocket implementation ---
|
||||
|
||||
get binaryType() {
|
||||
return this._binaryType;
|
||||
}
|
||||
|
||||
set binaryType(type) {
|
||||
this._binaryType = type;
|
||||
if (this._ws)
|
||||
this._ws.binaryType = type;
|
||||
}
|
||||
|
||||
get onclose() {
|
||||
return this._oncloseListener;
|
||||
}
|
||||
|
||||
set onclose(listener) {
|
||||
if (this._oncloseListener)
|
||||
this.removeEventListener('close', this._oncloseListener as any);
|
||||
this._oncloseListener = listener;
|
||||
if (this._oncloseListener)
|
||||
this.addEventListener('close', this._oncloseListener as any);
|
||||
}
|
||||
|
||||
get onerror() {
|
||||
return this._onerrorListener;
|
||||
}
|
||||
|
||||
set onerror(listener) {
|
||||
if (this._onerrorListener)
|
||||
this.removeEventListener('error', this._onerrorListener);
|
||||
this._onerrorListener = listener;
|
||||
if (this._onerrorListener)
|
||||
this.addEventListener('error', this._onerrorListener);
|
||||
}
|
||||
|
||||
get onopen() {
|
||||
return this._onopenListener;
|
||||
}
|
||||
|
||||
set onopen(listener) {
|
||||
if (this._onopenListener)
|
||||
this.removeEventListener('open', this._onopenListener);
|
||||
this._onopenListener = listener;
|
||||
if (this._onopenListener)
|
||||
this.addEventListener('open', this._onopenListener);
|
||||
}
|
||||
|
||||
get onmessage() {
|
||||
return this._onmessageListener;
|
||||
}
|
||||
|
||||
set onmessage(listener) {
|
||||
if (this._onmessageListener)
|
||||
this.removeEventListener('message', this._onmessageListener as any);
|
||||
this._onmessageListener = listener;
|
||||
if (this._onmessageListener)
|
||||
this.addEventListener('message', this._onmessageListener as any);
|
||||
}
|
||||
|
||||
send(message: WebSocketMessage): void {
|
||||
if (this.readyState === WebSocketMock.CONNECTING)
|
||||
throw new DOMException(`Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.`);
|
||||
if (this.readyState !== WebSocketMock.OPEN)
|
||||
throw new DOMException(`WebSocket is already in CLOSING or CLOSED state.`);
|
||||
if (this._passthrough)
|
||||
this._apiSendToServer(message);
|
||||
else
|
||||
messageToData(message, data => binding({ type: 'onMessageFromPage', id: this._id, data }));
|
||||
}
|
||||
|
||||
close(code?: number, reason?: string): void {
|
||||
if (code !== undefined && code !== 1000 && (code < 3000 || code > 4999))
|
||||
throw new DOMException(`Failed to execute 'close' on 'WebSocket': The close code must be either 1000, or between 3000 and 4999. ${code} is neither.`);
|
||||
if (this.readyState === WebSocketMock.OPEN || this.readyState === WebSocketMock.CONNECTING)
|
||||
this.readyState = WebSocketMock.CLOSING;
|
||||
if (this._ws)
|
||||
this._ws.close(code, reason);
|
||||
else
|
||||
this._onWSClose(code, reason, true);
|
||||
}
|
||||
|
||||
// --- methods called from the routing API ---
|
||||
|
||||
_apiEnsureOpened() {
|
||||
// This is called at the end of the route handler. If we did not connect to the server,
|
||||
// assume that websocket will be fully mocked. In this case, pretend that server
|
||||
// connection is established right away.
|
||||
if (!this._ws)
|
||||
this._ensureOpened();
|
||||
}
|
||||
|
||||
_apiSendToPage(message: WebSocketMessage) {
|
||||
// Calling "sendToPage()" from the route handler. Allow this for easier testing.
|
||||
this._ensureOpened();
|
||||
if (this.readyState !== WebSocketMock.OPEN)
|
||||
throw new DOMException(`WebSocket is already in CLOSING or CLOSED state.`);
|
||||
this.dispatchEvent(new MessageEvent('message', { data: message, origin: this._origin, cancelable: true }));
|
||||
}
|
||||
|
||||
_apiSendToServer(message: WebSocketMessage) {
|
||||
if (!this._ws)
|
||||
throw new Error('Cannot send a message before connecting to the server');
|
||||
if (this._ws.readyState === WebSocketMock.CONNECTING)
|
||||
this._wsBufferedMessages.push(message);
|
||||
else
|
||||
this._ws.send(message);
|
||||
}
|
||||
|
||||
_apiConnect() {
|
||||
if (this._ws)
|
||||
throw new Error('Can only connect to the server once');
|
||||
|
||||
this._ws = new NativeWebSocket(this.url, this._protocols);
|
||||
this._ws.binaryType = this._binaryType;
|
||||
|
||||
this._ws.onopen = () => {
|
||||
for (const message of this._wsBufferedMessages)
|
||||
this._ws!.send(message);
|
||||
this._wsBufferedMessages = [];
|
||||
this._ensureOpened();
|
||||
};
|
||||
|
||||
this._ws.onclose = event => {
|
||||
this._onWSClose(event.code, event.reason, event.wasClean);
|
||||
};
|
||||
|
||||
this._ws.onmessage = event => {
|
||||
if (this._passthrough)
|
||||
this._apiSendToPage(event.data);
|
||||
else
|
||||
messageToData(event.data, data => binding({ type: 'onMessageFromServer', id: this._id, data }));
|
||||
};
|
||||
|
||||
this._ws.onerror = () => {
|
||||
// We do not expose errors in the API, so short-curcuit the error event.
|
||||
const event = new Event('error', { cancelable: true });
|
||||
this.dispatchEvent(event);
|
||||
};
|
||||
}
|
||||
|
||||
// This method connects to the server, and passes all messages through,
|
||||
// as if WebSocketMock was not engaged.
|
||||
_apiPassThrough() {
|
||||
this._passthrough = true;
|
||||
this._apiConnect();
|
||||
}
|
||||
|
||||
_apiClose(code: number | undefined, reason: string | undefined, wasClean: boolean) {
|
||||
if (this.readyState !== WebSocketMock.CLOSED) {
|
||||
this.readyState = WebSocketMock.CLOSED;
|
||||
this.dispatchEvent(new CloseEvent('close', { code, reason, wasClean, cancelable: true }));
|
||||
}
|
||||
// Immediately close the real WS and imitate that it has closed.
|
||||
this._ws?.close(code, reason);
|
||||
this._cleanupWS();
|
||||
binding({ type: 'onClose', id: this._id, code, reason, wasClean });
|
||||
idToWebSocket.delete(this._id);
|
||||
}
|
||||
|
||||
// --- internals ---
|
||||
|
||||
_ensureOpened() {
|
||||
if (this.readyState !== WebSocketMock.CONNECTING)
|
||||
return;
|
||||
this.readyState = WebSocketMock.OPEN;
|
||||
this.dispatchEvent(new Event('open', { cancelable: true }));
|
||||
}
|
||||
|
||||
private _onWSClose(code: number | undefined, reason: string | undefined, wasClean: boolean) {
|
||||
this._cleanupWS();
|
||||
if (this.readyState !== WebSocketMock.CLOSED) {
|
||||
this.readyState = WebSocketMock.CLOSED;
|
||||
this.dispatchEvent(new CloseEvent('close', { code, reason, wasClean, cancelable: true }));
|
||||
}
|
||||
binding({ type: 'onClose', id: this._id, code, reason, wasClean });
|
||||
idToWebSocket.delete(this._id);
|
||||
}
|
||||
|
||||
private _cleanupWS() {
|
||||
if (!this._ws)
|
||||
return;
|
||||
this._ws.onopen = null;
|
||||
this._ws.onclose = null;
|
||||
this._ws.onmessage = null;
|
||||
this._ws.onerror = null;
|
||||
this._ws = undefined;
|
||||
this._wsBufferedMessages = [];
|
||||
}
|
||||
}
|
||||
globalThis.WebSocket = class WebSocket extends WebSocketMock {};
|
||||
}
|
||||
|
|
@ -35,6 +35,8 @@ export type Attribution = {
|
|||
};
|
||||
|
||||
import type { CallMetadata } from '@protocol/callMetadata';
|
||||
import type { Dialog } from './dialog';
|
||||
import type { Download } from './download';
|
||||
export type { CallMetadata } from '@protocol/callMetadata';
|
||||
|
||||
export class SdkObject extends EventEmitter {
|
||||
|
|
@ -62,6 +64,8 @@ export interface Instrumentation {
|
|||
onPageClose(page: Page): void;
|
||||
onBrowserOpen(browser: Browser): void;
|
||||
onBrowserClose(browser: Browser): void;
|
||||
onDialog(dialog: Dialog): void;
|
||||
onDownload(page: Page, download: Download): void;
|
||||
}
|
||||
|
||||
export interface InstrumentationListener {
|
||||
|
|
@ -73,6 +77,8 @@ export interface InstrumentationListener {
|
|||
onPageClose?(page: Page): void;
|
||||
onBrowserOpen?(browser: Browser): void;
|
||||
onBrowserClose?(browser: Browser): void;
|
||||
onDialog?(dialog: Dialog): void;
|
||||
onDownload?(page: Page, download: Download): void;
|
||||
}
|
||||
|
||||
export function createInstrumentation(): Instrumentation {
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@ export class Page extends SdkObject {
|
|||
progress.throwIfAborted();
|
||||
if (!handler.noWaitAfter) {
|
||||
progress.log(` locator handler has finished, waiting for ${asLocator(this.attribution.playwright.options.sdkLanguage, handler.selector)} to be hidden`);
|
||||
await this.mainFrame().waitForSelectorInternal(progress, handler.selector, { state: 'hidden' });
|
||||
await this.mainFrame().waitForSelectorInternal(progress, handler.selector, false, { state: 'hidden' });
|
||||
} else {
|
||||
progress.log(` locator handler has finished`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,32 +45,35 @@ export class Recorder implements InstrumentationListener, IRecorder {
|
|||
private _omitCallTracking = false;
|
||||
private _currentLanguage: Language;
|
||||
|
||||
static showInspector(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) {
|
||||
const params: channels.BrowserContextRecorderSupplementEnableParams = {};
|
||||
static async showInspector(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, recorderAppFactory: IRecorderAppFactory) {
|
||||
if (isUnderTest())
|
||||
params.language = process.env.TEST_INSPECTOR_LANGUAGE;
|
||||
Recorder.show(context, recorderAppFactory, params).catch(() => {});
|
||||
return await Recorder.show('actions', context, recorderAppFactory, params);
|
||||
}
|
||||
|
||||
static show(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<Recorder> {
|
||||
static showInspectorNoReply(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) {
|
||||
Recorder.showInspector(context, {}, recorderAppFactory).catch(() => {});
|
||||
}
|
||||
|
||||
static show(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise<Recorder> {
|
||||
let recorderPromise = (context as any)[recorderSymbol] as Promise<Recorder>;
|
||||
if (!recorderPromise) {
|
||||
recorderPromise = Recorder._create(context, recorderAppFactory, params);
|
||||
recorderPromise = Recorder._create(codegenMode, context, recorderAppFactory, params);
|
||||
(context as any)[recorderSymbol] = recorderPromise;
|
||||
}
|
||||
return recorderPromise;
|
||||
}
|
||||
|
||||
private static async _create(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<Recorder> {
|
||||
const recorder = new Recorder(context, params);
|
||||
private static async _create(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise<Recorder> {
|
||||
const recorder = new Recorder(codegenMode, context, params);
|
||||
const recorderApp = await recorderAppFactory(recorder);
|
||||
await recorder._install(recorderApp);
|
||||
return recorder;
|
||||
}
|
||||
|
||||
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
|
||||
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
|
||||
this._mode = params.mode || 'none';
|
||||
this._contextRecorder = new ContextRecorder(context, params, {});
|
||||
this._contextRecorder = new ContextRecorder(codegenMode, context, params, {});
|
||||
this._context = context;
|
||||
this._omitCallTracking = !!params.omitCallTracking;
|
||||
this._debugger = context.debugger();
|
||||
|
|
|
|||
|
|
@ -48,14 +48,14 @@ export class ContextRecorder extends EventEmitter {
|
|||
private _lastDialogOrdinal = -1;
|
||||
private _lastDownloadOrdinal = -1;
|
||||
private _context: BrowserContext;
|
||||
private _params: channels.BrowserContextRecorderSupplementEnableParams;
|
||||
private _params: channels.BrowserContextEnableRecorderParams;
|
||||
private _delegate: ContextRecorderDelegate;
|
||||
private _recorderSources: Source[];
|
||||
private _throttledOutputFile: ThrottledFile | null = null;
|
||||
private _orderedLanguages: LanguageGenerator[] = [];
|
||||
private _listeners: RegisteredListener[] = [];
|
||||
|
||||
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams, delegate: ContextRecorderDelegate) {
|
||||
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) {
|
||||
super();
|
||||
this._context = context;
|
||||
this._params = params;
|
||||
|
|
@ -73,11 +73,11 @@ export class ContextRecorder extends EventEmitter {
|
|||
saveStorage: params.saveStorage,
|
||||
};
|
||||
|
||||
const collection = new RecorderCollection(this._pageAliases, params.mode === 'recording');
|
||||
collection.on('change', () => {
|
||||
this._collection = new RecorderCollection(codegenMode, context, this._pageAliases);
|
||||
this._collection.on('change', (actions: ActionInContext[]) => {
|
||||
this._recorderSources = [];
|
||||
for (const languageGenerator of this._orderedLanguages) {
|
||||
const { header, footer, actionTexts, text } = generateCode(collection.actions(), languageGenerator, languageGeneratorOptions);
|
||||
const { header, footer, actionTexts, text } = generateCode(actions, languageGenerator, languageGeneratorOptions);
|
||||
const source: Source = {
|
||||
isRecorded: true,
|
||||
label: languageGenerator.name,
|
||||
|
|
@ -103,7 +103,7 @@ export class ContextRecorder extends EventEmitter {
|
|||
this._listeners.push(eventsHelper.addEventListener(process, 'exit', () => {
|
||||
this._throttledOutputFile?.flush();
|
||||
}));
|
||||
this._collection = collection;
|
||||
this.setEnabled(true);
|
||||
}
|
||||
|
||||
setOutput(codegenId: string, outputFile?: string) {
|
||||
|
|
@ -145,6 +145,10 @@ export class ContextRecorder extends EventEmitter {
|
|||
|
||||
setEnabled(enabled: boolean) {
|
||||
this._collection.setEnabled(enabled);
|
||||
if (enabled)
|
||||
this._context.tracing.startChunk({ name: 'trace', title: 'trace' }).catch(() => {});
|
||||
else
|
||||
this._context.tracing.stopChunk({ mode: 'discard' }).catch(() => {});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ declare global {
|
|||
}
|
||||
|
||||
export class EmptyRecorderApp extends EventEmitter implements IRecorderApp {
|
||||
wsEndpointForTest: undefined;
|
||||
async close(): Promise<void> {}
|
||||
async setPaused(paused: boolean): Promise<void> {}
|
||||
async setMode(mode: Mode): Promise<void> {}
|
||||
|
|
@ -54,7 +55,7 @@ export class EmptyRecorderApp extends EventEmitter implements IRecorderApp {
|
|||
|
||||
export class RecorderApp extends EventEmitter implements IRecorderApp {
|
||||
private _page: Page;
|
||||
readonly wsEndpoint: string | undefined;
|
||||
readonly wsEndpointForTest: string | undefined;
|
||||
private _recorder: IRecorder;
|
||||
|
||||
constructor(recorder: IRecorder, page: Page, wsEndpoint: string | undefined) {
|
||||
|
|
@ -62,7 +63,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
|
|||
this.setMaxListeners(0);
|
||||
this._recorder = recorder;
|
||||
this._page = page;
|
||||
this.wsEndpoint = wsEndpoint;
|
||||
this.wsEndpointForTest = wsEndpoint;
|
||||
}
|
||||
|
||||
async close() {
|
||||
|
|
@ -80,7 +81,6 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
|
|||
const file = require.resolve('../../vite/recorder/' + uri);
|
||||
fs.promises.readFile(file).then(buffer => {
|
||||
route.fulfill({
|
||||
requestUrl: route.request().url(),
|
||||
status: 200,
|
||||
headers: [
|
||||
{ name: 'Content-Type', value: mime.getType(path.extname(file)) || 'application/octet-stream' }
|
||||
|
|
@ -122,9 +122,8 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
|
|||
persistentContextOptions: {
|
||||
noDefaultViewport: true,
|
||||
headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed),
|
||||
useWebSocket: !!process.env.PWTEST_RECORDER_PORT,
|
||||
useWebSocket: isUnderTest(),
|
||||
handleSIGINT: false,
|
||||
args: process.env.PWTEST_RECORDER_PORT ? [`--remote-debugging-port=${process.env.PWTEST_RECORDER_PORT}`] : [],
|
||||
executablePath: inspectedContext._browser.options.isChromium ? inspectedContext._browser.options.customExecutablePath : undefined,
|
||||
}
|
||||
});
|
||||
|
|
@ -162,8 +161,10 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
|
|||
}).toString(), { isFunction: true }, sources).catch(() => {});
|
||||
|
||||
// Testing harness for runCLI mode.
|
||||
if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length)
|
||||
(process as any)._didSetSourcesForTest(sources[0].text);
|
||||
if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) {
|
||||
if ((process as any)._didSetSourcesForTest(sources[0].text))
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
async setSelector(selector: string, userGesture?: boolean): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -20,31 +20,35 @@ import type { Page } from '../page';
|
|||
import type { Signal } from './recorderActions';
|
||||
import type { ActionInContext } from '../codegen/types';
|
||||
import { monotonicTime } from '../../utils/time';
|
||||
import { callMetadataForAction } from './recorderUtils';
|
||||
import { callMetadataForAction, collapseActions, traceEventsToAction } from './recorderUtils';
|
||||
import { serializeError } from '../errors';
|
||||
import { performAction } from './recorderRunner';
|
||||
import type { CallMetadata } from '@protocol/callMetadata';
|
||||
import { isUnderTest } from '../../utils/debug';
|
||||
import type { BrowserContext } from '../browserContext';
|
||||
|
||||
export class RecorderCollection extends EventEmitter {
|
||||
private _actions: ActionInContext[] = [];
|
||||
private _enabled: boolean;
|
||||
private _enabled = false;
|
||||
private _pageAliases: Map<Page, string>;
|
||||
private _context: BrowserContext;
|
||||
|
||||
constructor(pageAliases: Map<Page, string>, enabled: boolean) {
|
||||
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, pageAliases: Map<Page, string>) {
|
||||
super();
|
||||
this._enabled = enabled;
|
||||
this._context = context;
|
||||
this._pageAliases = pageAliases;
|
||||
this.restart();
|
||||
|
||||
if (codegenMode === 'trace-events') {
|
||||
this._context.tracing.onMemoryEvents(events => {
|
||||
this._actions = traceEventsToAction(events);
|
||||
this._fireChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
restart() {
|
||||
this._actions = [];
|
||||
this.emit('change');
|
||||
}
|
||||
|
||||
actions() {
|
||||
return this._actions;
|
||||
this._fireChange();
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean) {
|
||||
|
|
@ -60,7 +64,7 @@ export class RecorderCollection extends EventEmitter {
|
|||
addRecordedAction(actionInContext: ActionInContext) {
|
||||
if (['openPage', 'closePage'].includes(actionInContext.action.name)) {
|
||||
this._actions.push(actionInContext);
|
||||
this.emit('change');
|
||||
this._fireChange();
|
||||
return;
|
||||
}
|
||||
this._addAction(actionInContext).catch(() => {});
|
||||
|
|
@ -69,11 +73,16 @@ export class RecorderCollection extends EventEmitter {
|
|||
private async _addAction(actionInContext: ActionInContext, callback?: (callMetadata: CallMetadata) => Promise<void>) {
|
||||
if (!this._enabled)
|
||||
return;
|
||||
if (actionInContext.action.name === 'openPage' || actionInContext.action.name === 'closePage') {
|
||||
this._actions.push(actionInContext);
|
||||
this._fireChange();
|
||||
return;
|
||||
}
|
||||
|
||||
const { callMetadata, mainFrame } = callMetadataForAction(this._pageAliases, actionInContext);
|
||||
await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata);
|
||||
this._actions.push(actionInContext);
|
||||
this.emit('change');
|
||||
this._fireChange();
|
||||
const error = await callback?.(callMetadata).catch((e: Error) => e);
|
||||
callMetadata.endTime = monotonicTime();
|
||||
callMetadata.error = error ? serializeError(error) : undefined;
|
||||
|
|
@ -116,8 +125,12 @@ export class RecorderCollection extends EventEmitter {
|
|||
|
||||
if (this._actions.length) {
|
||||
this._actions[this._actions.length - 1].action.signals.push(signal);
|
||||
this.emit('change');
|
||||
this._fireChange();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private _fireChange() {
|
||||
this.emit('change', collapseActions(this._actions));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export interface IRecorder {
|
|||
}
|
||||
|
||||
export interface IRecorderApp extends EventEmitter {
|
||||
readonly wsEndpointForTest: string | undefined;
|
||||
close(): Promise<void>;
|
||||
setPaused(paused: boolean): Promise<void>;
|
||||
setMode(mode: Mode): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -21,74 +21,97 @@ import type { IRecorder, IRecorderApp, IRecorderAppFactory } from './recorderFro
|
|||
import { installRootRedirect, openTraceViewerApp, startTraceViewerServer } from '../trace/viewer/traceViewer';
|
||||
import type { TraceViewerServerOptions } from '../trace/viewer/traceViewer';
|
||||
import type { BrowserContext } from '../browserContext';
|
||||
import { gracefullyProcessExitDoNotHang } from '../../utils/processLauncher';
|
||||
import type { Transport } from '../../utils/httpServer';
|
||||
import type { HttpServer, Transport } from '../../utils/httpServer';
|
||||
import type { Page } from '../page';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
|
||||
export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp {
|
||||
private _recorder: IRecorder;
|
||||
private _transport: Transport;
|
||||
readonly wsEndpointForTest: string | undefined;
|
||||
private _transport: RecorderTransport;
|
||||
private _tracePage: Page;
|
||||
private _traceServer: HttpServer;
|
||||
|
||||
static factory(context: BrowserContext): IRecorderAppFactory {
|
||||
return async (recorder: IRecorder) => {
|
||||
const transport = new RecorderTransport();
|
||||
const trace = path.join(context._browser.options.tracesDir, 'trace');
|
||||
await openApp(trace, { transport });
|
||||
return new RecorderInTraceViewer(context, recorder, transport);
|
||||
const { wsEndpointForTest, tracePage, traceServer } = await openApp(trace, { transport, headless: !context._browser.options.headful });
|
||||
return new RecorderInTraceViewer(transport, tracePage, traceServer, wsEndpointForTest);
|
||||
};
|
||||
}
|
||||
|
||||
constructor(context: BrowserContext, recorder: IRecorder, transport: Transport) {
|
||||
constructor(transport: RecorderTransport, tracePage: Page, traceServer: HttpServer, wsEndpointForTest: string | undefined) {
|
||||
super();
|
||||
this._recorder = recorder;
|
||||
this._transport = transport;
|
||||
this._tracePage = tracePage;
|
||||
this._traceServer = traceServer;
|
||||
this.wsEndpointForTest = wsEndpointForTest;
|
||||
this._tracePage.once('close', () => {
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
this._transport.sendEvent?.('close', {});
|
||||
await this._tracePage.context().close({ reason: 'Recorder window closed' });
|
||||
await this._traceServer.stop();
|
||||
}
|
||||
|
||||
async setPaused(paused: boolean): Promise<void> {
|
||||
this._transport.sendEvent?.('setPaused', { paused });
|
||||
this._transport.deliverEvent('setPaused', { paused });
|
||||
}
|
||||
|
||||
async setMode(mode: Mode): Promise<void> {
|
||||
this._transport.sendEvent?.('setMode', { mode });
|
||||
this._transport.deliverEvent('setMode', { mode });
|
||||
}
|
||||
|
||||
async setFile(file: string): Promise<void> {
|
||||
this._transport.sendEvent?.('setFileIfNeeded', { file });
|
||||
this._transport.deliverEvent('setFileIfNeeded', { file });
|
||||
}
|
||||
|
||||
async setSelector(selector: string, userGesture?: boolean): Promise<void> {
|
||||
this._transport.sendEvent?.('setSelector', { selector, userGesture });
|
||||
this._transport.deliverEvent('setSelector', { selector, userGesture });
|
||||
}
|
||||
|
||||
async updateCallLogs(callLogs: CallLog[]): Promise<void> {
|
||||
this._transport.sendEvent?.('updateCallLogs', { callLogs });
|
||||
this._transport.deliverEvent('updateCallLogs', { callLogs });
|
||||
}
|
||||
|
||||
async setSources(sources: Source[]): Promise<void> {
|
||||
this._transport.sendEvent?.('setSources', { sources });
|
||||
this._transport.deliverEvent('setSources', { sources });
|
||||
if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) {
|
||||
if ((process as any)._didSetSourcesForTest(sources[0].text))
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }) {
|
||||
const server = await startTraceViewerServer(options);
|
||||
await installRootRedirect(server, [trace], { ...options, webApp: 'recorder.html' });
|
||||
const page = await openTraceViewerApp(server.urlPrefix('precise'), 'chromium', options);
|
||||
page.on('close', () => gracefullyProcessExitDoNotHang(0));
|
||||
async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }): Promise<{ wsEndpointForTest: string | undefined, tracePage: Page, traceServer: HttpServer }> {
|
||||
const traceServer = await startTraceViewerServer(options);
|
||||
await installRootRedirect(traceServer, [trace], { ...options, webApp: 'recorder.html' });
|
||||
const page = await openTraceViewerApp(traceServer.urlPrefix('precise'), 'chromium', options);
|
||||
return { wsEndpointForTest: page.context()._browser.options.wsEndpoint, tracePage: page, traceServer };
|
||||
}
|
||||
|
||||
class RecorderTransport implements Transport {
|
||||
private _connected = new ManualPromise<void>();
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
async dispatch(method: string, params: any) {
|
||||
onconnect() {
|
||||
this._connected.resolve();
|
||||
}
|
||||
|
||||
async dispatch(method: string, params: any): Promise<any> {
|
||||
}
|
||||
|
||||
onclose() {
|
||||
}
|
||||
|
||||
deliverEvent(method: string, params: any) {
|
||||
this._connected.then(() => this.sendEvent?.(method, params));
|
||||
}
|
||||
|
||||
sendEvent?: (method: string, params: any) => void;
|
||||
close?: () => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,13 @@ import type { Page } from '../page';
|
|||
import type { ActionInContext } from '../codegen/types';
|
||||
import type { Frame } from '../frames';
|
||||
import type * as actions from './recorderActions';
|
||||
import { toKeyboardModifiers } from '../codegen/language';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type * as trace from '@trace/trace';
|
||||
import { fromKeyboardModifiers, toKeyboardModifiers } from '../codegen/language';
|
||||
import { serializeExpectedTextValues } from '../../utils/expectUtils';
|
||||
import { createGuid, monotonicTime } from '../../utils';
|
||||
import { parseSerializedValue, serializeValue } from '../../protocol/serializers';
|
||||
import type { SmartKeyboardModifier } from '../types';
|
||||
|
||||
export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog {
|
||||
let title = metadata.apiName || metadata.method;
|
||||
|
|
@ -76,76 +80,425 @@ export async function frameForAction(pageAliases: Map<Page, string>, actionInCon
|
|||
return result.frame;
|
||||
}
|
||||
|
||||
export function traceParamsForAction(actionInContext: ActionInContext) {
|
||||
export function traceParamsForAction(actionInContext: ActionInContext): { method: string, params: any } {
|
||||
const { action } = actionInContext;
|
||||
|
||||
switch (action.name) {
|
||||
case 'navigate': return { url: action.url };
|
||||
case 'openPage': return {};
|
||||
case 'closePage': return {};
|
||||
case 'navigate': {
|
||||
const params: channels.FrameGotoParams = {
|
||||
url: action.url,
|
||||
};
|
||||
return { method: 'goto', params };
|
||||
}
|
||||
case 'openPage':
|
||||
case 'closePage':
|
||||
throw new Error('Not reached');
|
||||
}
|
||||
|
||||
const selector = buildFullSelector(actionInContext.frame.framePath, action.selector);
|
||||
switch (action.name) {
|
||||
case 'click': return { selector, clickCount: action.clickCount };
|
||||
case 'press': {
|
||||
const modifiers = toKeyboardModifiers(action.modifiers);
|
||||
const shortcut = [...modifiers, action.key].join('+');
|
||||
return { selector, key: shortcut };
|
||||
}
|
||||
case 'fill': return { selector, text: action.text };
|
||||
case 'setInputFiles': return { selector, files: action.files };
|
||||
case 'check': return { selector };
|
||||
case 'uncheck': return { selector };
|
||||
case 'select': return { selector, values: action.options.map(value => ({ value })) };
|
||||
case 'assertChecked': {
|
||||
return {
|
||||
case 'click': {
|
||||
const params: channels.FrameClickParams = {
|
||||
selector,
|
||||
strict: true,
|
||||
modifiers: toKeyboardModifiers(action.modifiers),
|
||||
button: action.button,
|
||||
clickCount: action.clickCount,
|
||||
position: action.position,
|
||||
};
|
||||
return { method: 'click', params };
|
||||
}
|
||||
case 'press': {
|
||||
const params: channels.FramePressParams = {
|
||||
selector,
|
||||
strict: true,
|
||||
key: [...toKeyboardModifiers(action.modifiers), action.key].join('+'),
|
||||
};
|
||||
return { method: 'press', params };
|
||||
}
|
||||
case 'fill': {
|
||||
const params: channels.FrameFillParams = {
|
||||
selector,
|
||||
strict: true,
|
||||
value: action.text,
|
||||
};
|
||||
return { method: 'fill', params };
|
||||
}
|
||||
case 'setInputFiles': {
|
||||
const params: channels.FrameSetInputFilesParams = {
|
||||
selector,
|
||||
strict: true,
|
||||
localPaths: action.files,
|
||||
};
|
||||
return { method: 'setInputFiles', params };
|
||||
}
|
||||
case 'check': {
|
||||
const params: channels.FrameCheckParams = {
|
||||
selector,
|
||||
strict: true,
|
||||
};
|
||||
return { method: 'check', params };
|
||||
}
|
||||
case 'uncheck': {
|
||||
const params: channels.FrameUncheckParams = {
|
||||
selector,
|
||||
strict: true,
|
||||
};
|
||||
return { method: 'uncheck', params };
|
||||
}
|
||||
case 'select': {
|
||||
const params: channels.FrameSelectOptionParams = {
|
||||
selector,
|
||||
strict: true,
|
||||
options: action.options.map(option => ({ value: option })),
|
||||
};
|
||||
return { method: 'selectOption', params };
|
||||
}
|
||||
case 'assertChecked': {
|
||||
const params: channels.FrameExpectParams = {
|
||||
selector: action.selector,
|
||||
expression: 'to.be.checked',
|
||||
isNot: !action.checked,
|
||||
};
|
||||
return { method: 'expect', params };
|
||||
}
|
||||
case 'assertText': {
|
||||
return {
|
||||
const params: channels.FrameExpectParams = {
|
||||
selector,
|
||||
expression: 'to.have.text',
|
||||
expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }),
|
||||
expectedText: serializeExpectedTextValues([action.text], { matchSubstring: action.substring, normalizeWhiteSpace: true }),
|
||||
isNot: false,
|
||||
};
|
||||
return { method: 'expect', params };
|
||||
}
|
||||
case 'assertValue': {
|
||||
return {
|
||||
const params: channels.FrameExpectParams = {
|
||||
selector,
|
||||
expression: 'to.have.value',
|
||||
expectedValue: action.value,
|
||||
expectedValue: { value: serializeValue(action.value, value => ({ fallThrough: value })), handles: [] },
|
||||
isNot: false,
|
||||
};
|
||||
return { method: 'expect', params };
|
||||
}
|
||||
case 'assertVisible': {
|
||||
return {
|
||||
const params: channels.FrameExpectParams = {
|
||||
selector,
|
||||
expression: 'to.be.visible',
|
||||
isNot: false,
|
||||
};
|
||||
return { method: 'expect', params };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function callMetadataForAction(pageAliases: Map<Page, string>, actionInContext: ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } {
|
||||
const mainFrame = mainFrameForAction(pageAliases, actionInContext);
|
||||
const { action } = actionInContext;
|
||||
const { method, params } = traceParamsForAction(actionInContext);
|
||||
|
||||
const callMetadata: CallMetadata = {
|
||||
id: `call@${createGuid()}`,
|
||||
apiName: 'frame.' + action.name,
|
||||
stepId: `recorder@${createGuid()}`,
|
||||
apiName: 'page.' + method,
|
||||
objectId: mainFrame.guid,
|
||||
pageId: mainFrame._page.guid,
|
||||
frameId: mainFrame.guid,
|
||||
startTime: monotonicTime(),
|
||||
endTime: 0,
|
||||
type: 'Frame',
|
||||
method: action.name,
|
||||
params: traceParamsForAction(actionInContext),
|
||||
method,
|
||||
params,
|
||||
log: [],
|
||||
};
|
||||
return { callMetadata, mainFrame };
|
||||
}
|
||||
|
||||
export function traceEventsToAction(events: trace.TraceEvent[]): ActionInContext[] {
|
||||
const result: ActionInContext[] = [];
|
||||
const pageAliases = new Map<string, string>();
|
||||
let lastDownloadOrdinal = 0;
|
||||
let lastDialogOrdinal = 0;
|
||||
|
||||
const addSignal = (signal: actions.Signal) => {
|
||||
const lastAction = result[result.length - 1];
|
||||
if (!lastAction)
|
||||
return;
|
||||
lastAction.action.signals.push(signal);
|
||||
};
|
||||
|
||||
for (const event of events) {
|
||||
if (event.type === 'event' && event.class === 'BrowserContext') {
|
||||
const { method, params } = event;
|
||||
if (method === 'page') {
|
||||
const pageAlias = 'page' + (pageAliases.size || '');
|
||||
pageAliases.set(params.pageId, pageAlias);
|
||||
addSignal({
|
||||
name: 'popup',
|
||||
popupAlias: pageAlias,
|
||||
});
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'openPage',
|
||||
url: '',
|
||||
signals: [],
|
||||
},
|
||||
timestamp: event.time,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (method === 'pageClosed') {
|
||||
const pageAlias = pageAliases.get(event.params.pageId) || 'page';
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'closePage',
|
||||
signals: [],
|
||||
},
|
||||
timestamp: event.time,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (method === 'download') {
|
||||
const downloadAlias = lastDownloadOrdinal ? String(lastDownloadOrdinal) : '';
|
||||
++lastDownloadOrdinal;
|
||||
addSignal({
|
||||
name: 'download',
|
||||
downloadAlias,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (method === 'dialog') {
|
||||
const dialogAlias = lastDialogOrdinal ? String(lastDialogOrdinal) : '';
|
||||
++lastDialogOrdinal;
|
||||
addSignal({
|
||||
name: 'dialog',
|
||||
dialogAlias,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type !== 'before' || !event.pageId)
|
||||
continue;
|
||||
if (!event.stepId?.startsWith('recorder@'))
|
||||
continue;
|
||||
|
||||
const { method, params: untypedParams, pageId } = event;
|
||||
|
||||
let pageAlias = pageAliases.get(pageId);
|
||||
if (!pageAlias) {
|
||||
pageAlias = 'page';
|
||||
pageAliases.set(pageId, pageAlias);
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'openPage',
|
||||
url: '',
|
||||
signals: [],
|
||||
},
|
||||
timestamp: event.startTime,
|
||||
});
|
||||
}
|
||||
|
||||
if (method === 'goto') {
|
||||
const params = untypedParams as channels.FrameGotoParams;
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'navigate',
|
||||
url: params.url,
|
||||
signals: [],
|
||||
},
|
||||
timestamp: event.startTime,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (method === 'click') {
|
||||
const params = untypedParams as channels.FrameClickParams;
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'click',
|
||||
selector: params.selector,
|
||||
signals: [],
|
||||
button: params.button || 'left',
|
||||
modifiers: fromKeyboardModifiers(params.modifiers),
|
||||
clickCount: params.clickCount || 1,
|
||||
position: params.position,
|
||||
},
|
||||
timestamp: event.startTime
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (method === 'fill') {
|
||||
const params = untypedParams as channels.FrameFillParams;
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'fill',
|
||||
selector: params.selector,
|
||||
signals: [],
|
||||
text: params.value,
|
||||
},
|
||||
timestamp: event.startTime
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (method === 'press') {
|
||||
const params = untypedParams as channels.FramePressParams;
|
||||
const tokens = params.key.split('+');
|
||||
const modifiers = tokens.slice(0, tokens.length - 1) as SmartKeyboardModifier[];
|
||||
const key = tokens[tokens.length - 1];
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'press',
|
||||
selector: params.selector,
|
||||
signals: [],
|
||||
key,
|
||||
modifiers: fromKeyboardModifiers(modifiers),
|
||||
},
|
||||
timestamp: event.startTime
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (method === 'check') {
|
||||
const params = untypedParams as channels.FrameCheckParams;
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'check',
|
||||
selector: params.selector,
|
||||
signals: [],
|
||||
},
|
||||
timestamp: event.startTime
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (method === 'uncheck') {
|
||||
const params = untypedParams as channels.FrameUncheckParams;
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'uncheck',
|
||||
selector: params.selector,
|
||||
signals: [],
|
||||
},
|
||||
timestamp: event.startTime
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (method === 'selectOption') {
|
||||
const params = untypedParams as channels.FrameSelectOptionParams;
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'select',
|
||||
selector: params.selector,
|
||||
signals: [],
|
||||
options: (params.options || []).map(option => option.value!),
|
||||
},
|
||||
timestamp: event.startTime
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (method === 'setInputFiles') {
|
||||
const params = untypedParams as channels.FrameSetInputFilesParams;
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'setInputFiles',
|
||||
selector: params.selector,
|
||||
signals: [],
|
||||
files: params.localPaths || [],
|
||||
},
|
||||
timestamp: event.startTime
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (method === 'expect') {
|
||||
const params = untypedParams as channels.FrameExpectParams;
|
||||
if (params.expression === 'to.have.text') {
|
||||
const entry = params.expectedText?.[0];
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'assertText',
|
||||
selector: params.selector,
|
||||
signals: [],
|
||||
text: entry?.string!,
|
||||
substring: !!entry?.matchSubstring,
|
||||
},
|
||||
timestamp: event.startTime
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (params.expression === 'to.have.value') {
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'assertValue',
|
||||
selector: params.selector,
|
||||
signals: [],
|
||||
value: parseSerializedValue(params.expectedValue!.value, params.expectedValue!.handles),
|
||||
},
|
||||
timestamp: event.startTime
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (params.expression === 'to.be.checked') {
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'assertChecked',
|
||||
selector: params.selector,
|
||||
signals: [],
|
||||
checked: !params.isNot,
|
||||
},
|
||||
timestamp: event.startTime
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (params.expression === 'to.be.visible') {
|
||||
result.push({
|
||||
frame: { pageAlias, framePath: [] },
|
||||
action: {
|
||||
name: 'assertVisible',
|
||||
selector: params.selector,
|
||||
signals: [],
|
||||
},
|
||||
timestamp: event.startTime
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function collapseActions(actions: ActionInContext[]): ActionInContext[] {
|
||||
const result: ActionInContext[] = [];
|
||||
for (const action of actions) {
|
||||
const lastAction = result[result.length - 1];
|
||||
const isSameAction = lastAction && lastAction.action.name === action.action.name && lastAction.frame.pageAlias === action.frame.pageAlias && lastAction.frame.framePath.join('|') === action.frame.framePath.join('|');
|
||||
const isSameSelector = lastAction && 'selector' in lastAction.action && 'selector' in action.action && action.action.selector === lastAction.action.selector;
|
||||
const shouldMerge = isSameAction && (action.action.name === 'navigate' || (action.action.name === 'fill' && isSameSelector));
|
||||
if (!shouldMerge) {
|
||||
result.push(action);
|
||||
continue;
|
||||
}
|
||||
result[result.length - 1] = action;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -346,7 +346,7 @@ function rewriteToLocalhostIfNeeded(host: string): string {
|
|||
}
|
||||
|
||||
export function rewriteOpenSSLErrorIfNeeded(error: Error): Error {
|
||||
if (error.message !== 'unsupported')
|
||||
if (error.message !== 'unsupported' && (error as NodeJS.ErrnoException).code !== 'ERR_CRYPTO_UNSUPPORTED_OPERATION')
|
||||
return error;
|
||||
return rewriteErrorMessage(error, [
|
||||
'Unsupported TLS certificate.',
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ import { Snapshotter } from './snapshotter';
|
|||
import type { ConsoleMessage } from '../../console';
|
||||
import { Dispatcher } from '../../dispatchers/dispatcher';
|
||||
import { serializeError } from '../../errors';
|
||||
import type { Dialog } from '../../dialog';
|
||||
import type { Download } from '../../download';
|
||||
|
||||
const version: trace.VERSION = 7;
|
||||
|
||||
|
|
@ -79,6 +81,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||
private _allResources = new Set<string>();
|
||||
private _contextCreatedEvent: trace.ContextCreatedTraceEvent;
|
||||
private _pendingHarEntries = new Set<har.Entry>();
|
||||
private _inMemoryEvents: trace.TraceEvent[] | undefined;
|
||||
private _inMemoryEventsCallback: ((events: trace.TraceEvent[]) => void) | undefined;
|
||||
|
||||
constructor(context: BrowserContext | APIRequestContext, tracesDir: string | undefined) {
|
||||
super(context, 'tracing');
|
||||
|
|
@ -179,7 +183,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||
wallTime: Date.now(),
|
||||
monotonicTime: monotonicTime()
|
||||
};
|
||||
this._fs.appendFile(this._state.traceFile, JSON.stringify(event) + '\n');
|
||||
this._appendTraceEvent(event);
|
||||
|
||||
this._context.instrumentation.addListener(this, this._context);
|
||||
this._eventListeners.push(
|
||||
|
|
@ -193,6 +197,11 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||
return { traceName: this._state.traceName };
|
||||
}
|
||||
|
||||
onMemoryEvents(callback: (events: trace.TraceEvent[]) => void) {
|
||||
this._inMemoryEventsCallback = callback;
|
||||
this._inMemoryEvents = [];
|
||||
}
|
||||
|
||||
private _startScreencast() {
|
||||
if (!(this._context instanceof BrowserContext))
|
||||
return;
|
||||
|
|
@ -447,6 +456,50 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||
this._appendTraceEvent(event);
|
||||
}
|
||||
|
||||
onDialog(dialog: Dialog) {
|
||||
const event: trace.EventTraceEvent = {
|
||||
type: 'event',
|
||||
time: monotonicTime(),
|
||||
class: 'BrowserContext',
|
||||
method: 'dialog',
|
||||
params: { pageId: dialog.page().guid, type: dialog.type(), message: dialog.message(), defaultValue: dialog.defaultValue() },
|
||||
};
|
||||
this._appendTraceEvent(event);
|
||||
}
|
||||
|
||||
onDownload(page: Page, download: Download) {
|
||||
const event: trace.EventTraceEvent = {
|
||||
type: 'event',
|
||||
time: monotonicTime(),
|
||||
class: 'BrowserContext',
|
||||
method: 'download',
|
||||
params: { pageId: page.guid, url: download.url, suggestedFilename: download.suggestedFilename() },
|
||||
};
|
||||
this._appendTraceEvent(event);
|
||||
}
|
||||
|
||||
onPageOpen(page: Page) {
|
||||
const event: trace.EventTraceEvent = {
|
||||
type: 'event',
|
||||
time: monotonicTime(),
|
||||
class: 'BrowserContext',
|
||||
method: 'page',
|
||||
params: { pageId: page.guid, openerPageId: page.opener()?.guid },
|
||||
};
|
||||
this._appendTraceEvent(event);
|
||||
}
|
||||
|
||||
onPageClose(page: Page) {
|
||||
const event: trace.EventTraceEvent = {
|
||||
type: 'event',
|
||||
time: monotonicTime(),
|
||||
class: 'BrowserContext',
|
||||
method: 'pageClosed',
|
||||
params: { pageId: page.guid },
|
||||
};
|
||||
this._appendTraceEvent(event);
|
||||
}
|
||||
|
||||
private _onPageError(error: Error, page: Page) {
|
||||
const event: trace.EventTraceEvent = {
|
||||
type: 'event',
|
||||
|
|
@ -487,6 +540,10 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||
// Do not flush (console) events, they are too noisy, unless we are in ui mode (live).
|
||||
const flush = this._state!.options.live || (event.type !== 'event' && event.type !== 'console' && event.type !== 'log');
|
||||
this._fs.appendFile(this._state!.traceFile, JSON.stringify(visited) + '\n', flush);
|
||||
if (this._inMemoryEvents) {
|
||||
this._inMemoryEvents.push(event);
|
||||
this._inMemoryEventsCallback?.(this._inMemoryEvents);
|
||||
}
|
||||
}
|
||||
|
||||
private _appendResource(sha1: string, buffer: Buffer) {
|
||||
|
|
|
|||
|
|
@ -223,6 +223,9 @@ class StdinServer implements Transport {
|
|||
process.stdin.on('close', () => gracefullyProcessExitDoNotHang(0));
|
||||
}
|
||||
|
||||
onconnect() {
|
||||
}
|
||||
|
||||
async dispatch(method: string, params: any) {
|
||||
if (method === 'initialize') {
|
||||
if (this._traceUrl)
|
||||
|
|
|
|||
|
|
@ -27,8 +27,9 @@ export type ServerRouteHandler = (request: http.IncomingMessage, response: http.
|
|||
|
||||
export type Transport = {
|
||||
sendEvent?: (method: string, params: any) => void;
|
||||
dispatch: (method: string, params: any) => Promise<any>;
|
||||
close?: () => void;
|
||||
onconnect: () => void;
|
||||
dispatch: (method: string, params: any) => Promise<any>;
|
||||
onclose: () => void;
|
||||
};
|
||||
|
||||
|
|
@ -82,6 +83,7 @@ export class HttpServer {
|
|||
this._wsGuid = guid || createGuid();
|
||||
const wss = new wsServer({ server: this._server, path: '/' + this._wsGuid });
|
||||
wss.on('connection', ws => {
|
||||
transport.onconnect();
|
||||
transport.sendEvent = (method, params) => ws.send(JSON.stringify({ method, params }));
|
||||
transport.close = () => ws.close();
|
||||
ws.on('message', async message => {
|
||||
|
|
|
|||
|
|
@ -50,16 +50,6 @@ export function asLocators(lang: Language, selector: string, isFrameLocator: boo
|
|||
|
||||
function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFrameLocator: boolean = false, maxOutputSize = 20): string[] {
|
||||
const parts = [...parsed.parts];
|
||||
// frameLocator('iframe').first is actually "iframe >> nth=0 >> internal:control=enter-frame"
|
||||
// To make it easier to parse, we turn it into "iframe >> internal:control=enter-frame >> nth=0"
|
||||
for (let index = 0; index < parts.length - 1; index++) {
|
||||
if (parts[index].name === 'nth' && parts[index + 1].name === 'internal:control' && (parts[index + 1].body as string) === 'enter-frame') {
|
||||
// Swap nth and enter-frame.
|
||||
const [nth] = parts.splice(index, 1);
|
||||
parts.splice(index + 1, 0, nth);
|
||||
}
|
||||
}
|
||||
|
||||
const tokens: string[][] = [];
|
||||
let nextBase: LocatorBase = isFrameLocator ? 'frame-locator' : 'page';
|
||||
for (let index = 0; index < parts.length; index++) {
|
||||
|
|
@ -167,15 +157,15 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
|
|||
continue;
|
||||
}
|
||||
}
|
||||
if (part.name === 'internal:control' && (part.body as string) === 'enter-frame') {
|
||||
tokens.push([factory.generateLocator(base, 'frame', '')]);
|
||||
nextBase = 'frame-locator';
|
||||
continue;
|
||||
}
|
||||
|
||||
let locatorType: LocatorType = 'default';
|
||||
|
||||
const nextPart = parts[index + 1];
|
||||
if (nextPart && nextPart.name === 'internal:control' && (nextPart.body as string) === 'enter-frame') {
|
||||
locatorType = 'frame';
|
||||
nextBase = 'frame-locator';
|
||||
index++;
|
||||
}
|
||||
|
||||
const selectorPart = stringifySelector({ parts: [part] });
|
||||
const locatorPart = factory.generateLocator(base, locatorType, selectorPart);
|
||||
|
|
@ -264,7 +254,7 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
|||
return `locator(${this.quote(body as string)}, { hasNotText: ${this.toHasText(options.hasNotText)} })`;
|
||||
return `locator(${this.quote(body as string)})`;
|
||||
case 'frame':
|
||||
return `frameLocator(${this.quote(body as string)})`;
|
||||
return `contentFrame()`;
|
||||
case 'nth':
|
||||
return `nth(${body})`;
|
||||
case 'first':
|
||||
|
|
@ -356,7 +346,7 @@ export class PythonLocatorFactory implements LocatorFactory {
|
|||
return `locator(${this.quote(body as string)}, has_not_text=${this.toHasText(options.hasNotText)})`;
|
||||
return `locator(${this.quote(body as string)})`;
|
||||
case 'frame':
|
||||
return `frame_locator(${this.quote(body as string)})`;
|
||||
return `content_frame`;
|
||||
case 'nth':
|
||||
return `nth(${body})`;
|
||||
case 'first':
|
||||
|
|
@ -461,7 +451,7 @@ export class JavaLocatorFactory implements LocatorFactory {
|
|||
return `locator(${this.quote(body as string)}, new ${clazz}.LocatorOptions().setHasNotText(${this.toHasText(options.hasNotText)}))`;
|
||||
return `locator(${this.quote(body as string)})`;
|
||||
case 'frame':
|
||||
return `frameLocator(${this.quote(body as string)})`;
|
||||
return `contentFrame()`;
|
||||
case 'nth':
|
||||
return `nth(${body})`;
|
||||
case 'first':
|
||||
|
|
@ -556,7 +546,7 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
|||
return `Locator(${this.quote(body as string)}, new() { ${this.toHasNotText(options.hasNotText)} })`;
|
||||
return `Locator(${this.quote(body as string)})`;
|
||||
case 'frame':
|
||||
return `FrameLocator(${this.quote(body as string)})`;
|
||||
return `ContentFrame`;
|
||||
case 'nth':
|
||||
return `Nth(${body})`;
|
||||
case 'first':
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ function parseLocator(locator: string, testIdAttributeName: string): { selector:
|
|||
.replace(/has_text/g, 'hastext')
|
||||
.replace(/has_not/g, 'hasnot')
|
||||
.replace(/frame_locator/g, 'framelocator')
|
||||
.replace(/content_frame/g, 'contentframe')
|
||||
.replace(/[{}\s]/g, '')
|
||||
.replace(/new\(\)/g, '')
|
||||
.replace(/new[\w]+\.[\w]+options\(\)/g, '')
|
||||
|
|
@ -154,6 +155,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
|||
template = template
|
||||
.replace(/\,set([\w]+)\(([^)]+)\)/g, (_, group1, group2) => ',' + group1.toLowerCase() + '=' + group2.toLowerCase())
|
||||
.replace(/framelocator\(([^)]+)\)/g, '$1.internal:control=enter-frame')
|
||||
.replace(/contentframe(\(\))?/g, 'internal:control=enter-frame')
|
||||
.replace(/locator\(([^)]+),hastext=([^),]+)\)/g, 'locator($1).internal:has-text=$2')
|
||||
.replace(/locator\(([^)]+),hasnottext=([^),]+)\)/g, 'locator($1).internal:has-not-text=$2')
|
||||
.replace(/locator\(([^)]+),hastext=([^),]+)\)/g, 'locator($1).internal:has-text=$2')
|
||||
|
|
|
|||
87
packages/playwright-core/types/protocol.d.ts
vendored
87
packages/playwright-core/types/protocol.d.ts
vendored
|
|
@ -840,7 +840,7 @@ CORS RFC1918 enforcement.
|
|||
resourceIPAddressSpace?: Network.IPAddressSpace;
|
||||
clientSecurityState?: Network.ClientSecurityState;
|
||||
}
|
||||
export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader";
|
||||
export type AttributionReportingIssueType = "PermissionPolicyDisabled"|"UntrustworthyReportingOrigin"|"InsecureContext"|"InvalidHeader"|"InvalidRegisterTriggerHeader"|"SourceAndTriggerHeaders"|"SourceIgnored"|"TriggerIgnored"|"OsSourceIgnored"|"OsTriggerIgnored"|"InvalidRegisterOsSourceHeader"|"InvalidRegisterOsTriggerHeader"|"WebAndOsHeaders"|"NoWebOrOsSupport"|"NavigationRegistrationWithoutTransientUserActivation"|"InvalidInfoHeader"|"NoRegisterSourceHeader"|"NoRegisterTriggerHeader"|"NoRegisterOsSourceHeader"|"NoRegisterOsTriggerHeader"|"NavigationRegistrationUniqueScopeAlreadySet";
|
||||
export type SharedDictionaryError = "UseErrorCrossOriginNoCorsRequest"|"UseErrorDictionaryLoadFailure"|"UseErrorMatchingDictionaryNotUsed"|"UseErrorUnexpectedContentDictionaryHeader"|"WriteErrorCossOriginNoCorsRequest"|"WriteErrorDisallowedBySettings"|"WriteErrorExpiredResponse"|"WriteErrorFeatureDisabled"|"WriteErrorInsufficientResources"|"WriteErrorInvalidMatchField"|"WriteErrorInvalidStructuredHeader"|"WriteErrorNavigationRequest"|"WriteErrorNoMatchField"|"WriteErrorNonListMatchDestField"|"WriteErrorNonSecureContext"|"WriteErrorNonStringIdField"|"WriteErrorNonStringInMatchDestList"|"WriteErrorNonStringMatchField"|"WriteErrorNonTokenTypeField"|"WriteErrorRequestAborted"|"WriteErrorShuttingDown"|"WriteErrorTooLongIdField"|"WriteErrorUnsupportedType";
|
||||
/**
|
||||
* Details for issues around "Attribution Reporting API" usage.
|
||||
|
|
@ -1534,7 +1534,7 @@ events afterwards if enabled and recording.
|
|||
*/
|
||||
windowState?: WindowState;
|
||||
}
|
||||
export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"windowManagement";
|
||||
export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"windowManagement";
|
||||
export type PermissionSetting = "granted"|"denied"|"prompt";
|
||||
/**
|
||||
* Definition of PermissionDescriptor defined in the Permissions API:
|
||||
|
|
@ -3561,7 +3561,7 @@ front-end.
|
|||
/**
|
||||
* Pseudo element type.
|
||||
*/
|
||||
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new";
|
||||
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"select-fallback-button"|"select-fallback-button-text"|"picker";
|
||||
/**
|
||||
* Shadow root type.
|
||||
*/
|
||||
|
|
@ -3710,6 +3710,7 @@ The property is always undefined now.
|
|||
isSVG?: boolean;
|
||||
compatibilityMode?: CompatibilityMode;
|
||||
assignedSlot?: BackendNode;
|
||||
isScrollable?: boolean;
|
||||
}
|
||||
/**
|
||||
* A structure to hold the top-level node of a detached tree and an array of its retained descendants.
|
||||
|
|
@ -3954,6 +3955,19 @@ The property is always undefined now.
|
|||
* Called when top layer elements are changed.
|
||||
*/
|
||||
export type topLayerElementsUpdatedPayload = void;
|
||||
/**
|
||||
* Fired when a node's scrollability state changes.
|
||||
*/
|
||||
export type scrollableFlagUpdatedPayload = {
|
||||
/**
|
||||
* The id of the node.
|
||||
*/
|
||||
nodeId: DOM.NodeId;
|
||||
/**
|
||||
* If the node is scrollable.
|
||||
*/
|
||||
isScrollable: boolean;
|
||||
}
|
||||
/**
|
||||
* Called when a pseudo element is removed from an element.
|
||||
*/
|
||||
|
|
@ -8102,8 +8116,25 @@ or hexadecimal (0x prefixed) string.
|
|||
*/
|
||||
size: number;
|
||||
}
|
||||
/**
|
||||
* DOM object counter data.
|
||||
*/
|
||||
export interface DOMCounter {
|
||||
/**
|
||||
* Object name. Note: object names should be presumed volatile and clients should not expect
|
||||
the returned names to be consistent across runs.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Object count.
|
||||
*/
|
||||
count: number;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retruns current DOM object counters.
|
||||
*/
|
||||
export type getDOMCountersParameters = {
|
||||
}
|
||||
export type getDOMCountersReturnValue = {
|
||||
|
|
@ -8111,6 +8142,21 @@ or hexadecimal (0x prefixed) string.
|
|||
nodes: number;
|
||||
jsEventListeners: number;
|
||||
}
|
||||
/**
|
||||
* Retruns DOM object counters after preparing renderer for leak detection.
|
||||
*/
|
||||
export type getDOMCountersForLeakDetectionParameters = {
|
||||
}
|
||||
export type getDOMCountersForLeakDetectionReturnValue = {
|
||||
/**
|
||||
* DOM object counters.
|
||||
*/
|
||||
counters: DOMCounter[];
|
||||
}
|
||||
/**
|
||||
* Prepares for leak detection by terminating workers, stopping spellcheckers,
|
||||
dropping non-essential internal caches, running garbage collections, etc.
|
||||
*/
|
||||
export type prepareForLeakDetectionParameters = {
|
||||
}
|
||||
export type prepareForLeakDetectionReturnValue = {
|
||||
|
|
@ -8902,7 +8948,7 @@ This is a temporary ability and it will be removed in the future.
|
|||
/**
|
||||
* Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request.
|
||||
*/
|
||||
export type CookieExemptionReason = "None"|"UserSetting"|"TPCDMetadata"|"TPCDDeprecationTrial"|"TPCDHeuristics"|"EnterprisePolicy"|"StorageAccess"|"TopLevelStorageAccess"|"CorsOptIn"|"Scheme";
|
||||
export type CookieExemptionReason = "None"|"UserSetting"|"TPCDMetadata"|"TPCDDeprecationTrial"|"TopLevelTPCDDeprecationTrial"|"TPCDHeuristics"|"EnterprisePolicy"|"StorageAccess"|"TopLevelStorageAccess"|"Scheme";
|
||||
/**
|
||||
* A cookie which was not stored from a response with the corresponding reason.
|
||||
*/
|
||||
|
|
@ -11452,7 +11498,7 @@ as an ad.
|
|||
* All Permissions Policy features. This enum should match the one defined
|
||||
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
|
||||
*/
|
||||
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
|
||||
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
|
||||
/**
|
||||
* Reason for a permissions policy feature to be disabled.
|
||||
*/
|
||||
|
|
@ -12040,7 +12086,7 @@ https://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-expl
|
|||
/**
|
||||
* List of not restored reasons for back-forward cache.
|
||||
*/
|
||||
export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient";
|
||||
export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"ContentDiscarded"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient";
|
||||
/**
|
||||
* Types of not restored reasons for back-forward cache.
|
||||
*/
|
||||
|
|
@ -12151,6 +12197,16 @@ dependent on the reason:
|
|||
frameId: FrameId;
|
||||
reason: "remove"|"swap";
|
||||
}
|
||||
/**
|
||||
* Fired before frame subtree is detached. Emitted before any frame of the
|
||||
subtree is actually detached.
|
||||
*/
|
||||
export type frameSubtreeWillBeDetachedPayload = {
|
||||
/**
|
||||
* Id of the frame that is the root of the subtree that will be detached.
|
||||
*/
|
||||
frameId: FrameId;
|
||||
}
|
||||
/**
|
||||
* Fired once navigation of the frame has completed. Frame is now associated with the new loader.
|
||||
*/
|
||||
|
|
@ -14250,6 +14306,15 @@ int, only present for source registrations
|
|||
debugData: AttributionReportingAggregatableDebugReportingData[];
|
||||
aggregationCoordinatorOrigin?: string;
|
||||
}
|
||||
export interface AttributionScopesData {
|
||||
values: string[];
|
||||
/**
|
||||
* number instead of integer because not all uint32 can be represented by
|
||||
int
|
||||
*/
|
||||
limit: number;
|
||||
maxEventStates: number;
|
||||
}
|
||||
export interface AttributionReportingSourceRegistration {
|
||||
time: Network.TimeSinceEpoch;
|
||||
/**
|
||||
|
|
@ -14273,8 +14338,9 @@ int, only present for source registrations
|
|||
triggerDataMatching: AttributionReportingTriggerDataMatching;
|
||||
destinationLimitPriority: SignedInt64AsBase10;
|
||||
aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig;
|
||||
scopesData?: AttributionScopesData;
|
||||
}
|
||||
export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"destinationPerDayReportingLimitReached";
|
||||
export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxScopesChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"exceedsMaxEventStatesLimit"|"destinationPerDayReportingLimitReached";
|
||||
export type AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude";
|
||||
export interface AttributionReportingAggregatableValueDictEntry {
|
||||
key: string;
|
||||
|
|
@ -14317,6 +14383,7 @@ int
|
|||
sourceRegistrationTimeConfig: AttributionReportingSourceRegistrationTimeConfig;
|
||||
triggerContextId?: string;
|
||||
aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig;
|
||||
scopes: string[];
|
||||
}
|
||||
export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData";
|
||||
export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports";
|
||||
|
|
@ -17019,7 +17086,7 @@ status is shared by prefetchStatusUpdated and prerenderStatusUpdated.
|
|||
* TODO(https://crbug.com/1384419): revisit the list of PrefetchStatus and
|
||||
filter out the ones that aren't necessary to the developers.
|
||||
*/
|
||||
export type PrefetchStatus = "PrefetchAllowed"|"PrefetchFailedIneligibleRedirect"|"PrefetchFailedInvalidRedirect"|"PrefetchFailedMIMENotSupported"|"PrefetchFailedNetError"|"PrefetchFailedNon2XX"|"PrefetchFailedPerPageLimitExceeded"|"PrefetchEvictedAfterCandidateRemoved"|"PrefetchEvictedForNewerPrefetch"|"PrefetchHeldback"|"PrefetchIneligibleRetryAfter"|"PrefetchIsPrivacyDecoy"|"PrefetchIsStale"|"PrefetchNotEligibleBrowserContextOffTheRecord"|"PrefetchNotEligibleDataSaverEnabled"|"PrefetchNotEligibleExistingProxy"|"PrefetchNotEligibleHostIsNonUnique"|"PrefetchNotEligibleNonDefaultStoragePartition"|"PrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy"|"PrefetchNotEligibleSchemeIsNotHttps"|"PrefetchNotEligibleUserHasCookies"|"PrefetchNotEligibleUserHasServiceWorker"|"PrefetchNotEligibleBatterySaverEnabled"|"PrefetchNotEligiblePreloadingDisabled"|"PrefetchNotFinishedInTime"|"PrefetchNotStarted"|"PrefetchNotUsedCookiesChanged"|"PrefetchProxyNotAvailable"|"PrefetchResponseUsed"|"PrefetchSuccessfulButNotUsed"|"PrefetchNotUsedProbeFailed";
|
||||
export type PrefetchStatus = "PrefetchAllowed"|"PrefetchFailedIneligibleRedirect"|"PrefetchFailedInvalidRedirect"|"PrefetchFailedMIMENotSupported"|"PrefetchFailedNetError"|"PrefetchFailedNon2XX"|"PrefetchEvictedAfterCandidateRemoved"|"PrefetchEvictedForNewerPrefetch"|"PrefetchHeldback"|"PrefetchIneligibleRetryAfter"|"PrefetchIsPrivacyDecoy"|"PrefetchIsStale"|"PrefetchNotEligibleBrowserContextOffTheRecord"|"PrefetchNotEligibleDataSaverEnabled"|"PrefetchNotEligibleExistingProxy"|"PrefetchNotEligibleHostIsNonUnique"|"PrefetchNotEligibleNonDefaultStoragePartition"|"PrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy"|"PrefetchNotEligibleSchemeIsNotHttps"|"PrefetchNotEligibleUserHasCookies"|"PrefetchNotEligibleUserHasServiceWorker"|"PrefetchNotEligibleBatterySaverEnabled"|"PrefetchNotEligiblePreloadingDisabled"|"PrefetchNotFinishedInTime"|"PrefetchNotStarted"|"PrefetchNotUsedCookiesChanged"|"PrefetchProxyNotAvailable"|"PrefetchResponseUsed"|"PrefetchSuccessfulButNotUsed"|"PrefetchNotUsedProbeFailed";
|
||||
/**
|
||||
* Information of headers to be displayed when the header mismatch occurred.
|
||||
*/
|
||||
|
|
@ -20115,6 +20182,7 @@ Error was thrown.
|
|||
"DOM.inlineStyleInvalidated": DOM.inlineStyleInvalidatedPayload;
|
||||
"DOM.pseudoElementAdded": DOM.pseudoElementAddedPayload;
|
||||
"DOM.topLayerElementsUpdated": DOM.topLayerElementsUpdatedPayload;
|
||||
"DOM.scrollableFlagUpdated": DOM.scrollableFlagUpdatedPayload;
|
||||
"DOM.pseudoElementRemoved": DOM.pseudoElementRemovedPayload;
|
||||
"DOM.setChildNodes": DOM.setChildNodesPayload;
|
||||
"DOM.shadowRootPopped": DOM.shadowRootPoppedPayload;
|
||||
|
|
@ -20173,6 +20241,7 @@ Error was thrown.
|
|||
"Page.frameAttached": Page.frameAttachedPayload;
|
||||
"Page.frameClearedScheduledNavigation": Page.frameClearedScheduledNavigationPayload;
|
||||
"Page.frameDetached": Page.frameDetachedPayload;
|
||||
"Page.frameSubtreeWillBeDetached": Page.frameSubtreeWillBeDetachedPayload;
|
||||
"Page.frameNavigated": Page.frameNavigatedPayload;
|
||||
"Page.documentOpened": Page.documentOpenedPayload;
|
||||
"Page.frameResized": Page.frameResizedPayload;
|
||||
|
|
@ -20539,6 +20608,7 @@ Error was thrown.
|
|||
"Log.startViolationsReport": Log.startViolationsReportParameters;
|
||||
"Log.stopViolationsReport": Log.stopViolationsReportParameters;
|
||||
"Memory.getDOMCounters": Memory.getDOMCountersParameters;
|
||||
"Memory.getDOMCountersForLeakDetection": Memory.getDOMCountersForLeakDetectionParameters;
|
||||
"Memory.prepareForLeakDetection": Memory.prepareForLeakDetectionParameters;
|
||||
"Memory.forciblyPurgeJavaScriptMemory": Memory.forciblyPurgeJavaScriptMemoryParameters;
|
||||
"Memory.setPressureNotificationsSuppressed": Memory.setPressureNotificationsSuppressedParameters;
|
||||
|
|
@ -21148,6 +21218,7 @@ Error was thrown.
|
|||
"Log.startViolationsReport": Log.startViolationsReportReturnValue;
|
||||
"Log.stopViolationsReport": Log.stopViolationsReportReturnValue;
|
||||
"Memory.getDOMCounters": Memory.getDOMCountersReturnValue;
|
||||
"Memory.getDOMCountersForLeakDetection": Memory.getDOMCountersForLeakDetectionReturnValue;
|
||||
"Memory.prepareForLeakDetection": Memory.prepareForLeakDetectionReturnValue;
|
||||
"Memory.forciblyPurgeJavaScriptMemory": Memory.forciblyPurgeJavaScriptMemoryReturnValue;
|
||||
"Memory.setPressureNotificationsSuppressed": Memory.setPressureNotificationsSuppressedReturnValue;
|
||||
|
|
|
|||
199
packages/playwright-core/types/types.d.ts
vendored
199
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -3826,6 +3826,34 @@ export interface Page {
|
|||
url?: string|RegExp;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* This method allows to modify websocket connections that are made by the page.
|
||||
*
|
||||
* Note that only `WebSocket`s created after this method was called will be routed. It is recommended to call this
|
||||
* method before navigating the page.
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
* Below is an example of a simple handler that blocks some websocket messages. See {@link WebSocketRoute} for more
|
||||
* details and examples.
|
||||
*
|
||||
* ```js
|
||||
* await page.routeWebSocket('/ws', async ws => {
|
||||
* ws.routeSend(message => {
|
||||
* if (message === 'to-be-blocked')
|
||||
* return;
|
||||
* ws.send(message);
|
||||
* });
|
||||
* await ws.connect();
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the
|
||||
* `baseURL` from the context options.
|
||||
* @param handler Handler function to route the WebSocket.
|
||||
*/
|
||||
routeWebSocket(url: string|RegExp|((url: URL) => boolean), handler: ((websocketroute: WebSocketRoute) => Promise<any>|any)): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns the buffer with the captured screenshot.
|
||||
* @param options
|
||||
|
|
@ -8658,6 +8686,34 @@ export interface BrowserContext {
|
|||
url?: string|RegExp;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* This method allows to modify websocket connections that are made by any page in the browser context.
|
||||
*
|
||||
* Note that only `WebSocket`s created after this method was called will be routed. It is recommended to call this
|
||||
* method before creating any pages.
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
* Below is an example of a simple handler that blocks some websocket messages. See {@link WebSocketRoute} for more
|
||||
* details and examples.
|
||||
*
|
||||
* ```js
|
||||
* await context.routeWebSocket('/ws', async ws => {
|
||||
* ws.routeSend(message => {
|
||||
* if (message === 'to-be-blocked')
|
||||
* return;
|
||||
* ws.send(message);
|
||||
* });
|
||||
* await ws.connect();
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param url Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the
|
||||
* `baseURL` from the context options.
|
||||
* @param handler Handler function to route the WebSocket.
|
||||
*/
|
||||
routeWebSocket(url: string|RegExp|((url: URL) => boolean), handler: ((websocketroute: WebSocketRoute) => Promise<any>|any)): Promise<void>;
|
||||
|
||||
/**
|
||||
* **NOTE** Service workers are only supported on Chromium-based browsers.
|
||||
*
|
||||
|
|
@ -14567,6 +14623,134 @@ export interface CDPSession {
|
|||
detach(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever a [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) route is set up with
|
||||
* [page.routeWebSocket(url, handler)](https://playwright.dev/docs/api/class-page#page-route-web-socket) or
|
||||
* [browserContext.routeWebSocket(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browser-context-route-web-socket),
|
||||
* the `WebSocketRoute` object allows to handle the WebSocket.
|
||||
*
|
||||
* By default, the routed WebSocket will not actually connect to the server. This way, you can mock entire
|
||||
* communcation over the WebSocket. Here is an example that responds to a `"query"` with a `"result"`.
|
||||
*
|
||||
* ```js
|
||||
* await page.routeWebSocket('/ws', async ws => {
|
||||
* ws.routeSend(message => {
|
||||
* if (message === 'query')
|
||||
* ws.receive('result');
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
export interface WebSocketRoute {
|
||||
/**
|
||||
* This method allows to route messages that are sent by `WebSocket.send()` call in the page, instead of actually
|
||||
* sending them to the server. Once this method is called, sent messages **are not** automatically forwarded to the
|
||||
* server - you should do that manually by calling
|
||||
* [webSocketRoute.send(message)](https://playwright.dev/docs/api/class-websocketroute#web-socket-route-send).
|
||||
*
|
||||
* Calling this method again times will override the handler with a new one.
|
||||
* @param handler Handler function to route sent messages.
|
||||
*/
|
||||
routeSend(handler: (message: string | Buffer) => any): void;
|
||||
/**
|
||||
* This method allows to route messages that are received by the
|
||||
* [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page from the server. This
|
||||
* method only makes sense if you are also calling
|
||||
* [webSocketRoute.connect()](https://playwright.dev/docs/api/class-websocketroute#web-socket-route-connect).
|
||||
*
|
||||
* Once this method is called, received messages are not automatically dispatched to the
|
||||
* [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page - you should do that
|
||||
* manually by calling
|
||||
* [webSocketRoute.receive(message)](https://playwright.dev/docs/api/class-websocketroute#web-socket-route-receive).
|
||||
*
|
||||
* Calling this method again times will override the handler with a new one.
|
||||
* @param handler Handler function to route received messages.
|
||||
*/
|
||||
routeReceive(handler: (message: string | Buffer) => any): void;
|
||||
/**
|
||||
* Emitted when the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) closes.
|
||||
*/
|
||||
on(event: 'close', listener: () => any): this;
|
||||
|
||||
/**
|
||||
* Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.
|
||||
*/
|
||||
once(event: 'close', listener: () => any): this;
|
||||
|
||||
/**
|
||||
* Emitted when the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) closes.
|
||||
*/
|
||||
addListener(event: 'close', listener: () => any): this;
|
||||
|
||||
/**
|
||||
* Removes an event listener added by `on` or `addListener`.
|
||||
*/
|
||||
removeListener(event: 'close', listener: () => any): this;
|
||||
|
||||
/**
|
||||
* Removes an event listener added by `on` or `addListener`.
|
||||
*/
|
||||
off(event: 'close', listener: () => any): this;
|
||||
|
||||
/**
|
||||
* Emitted when the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) closes.
|
||||
*/
|
||||
prependListener(event: 'close', listener: () => any): this;
|
||||
|
||||
/**
|
||||
* Closes the server connection and the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
||||
* object in the page.
|
||||
* @param options
|
||||
*/
|
||||
close(options?: {
|
||||
/**
|
||||
* Optional [close code](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#code).
|
||||
*/
|
||||
code?: number;
|
||||
|
||||
/**
|
||||
* Optional [close reason](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#reason).
|
||||
*/
|
||||
reason?: string;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* By default, routed WebSocket does not connect to the server, so you can mock entire WebSocket communication. This
|
||||
* method connects to the actual WebSocket server, giving the ability to send and receive messages from the server.
|
||||
*
|
||||
* Once connected:
|
||||
* - Messages received from the server will be automatically dispatched to the
|
||||
* [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the page, unless
|
||||
* [webSocketRoute.routeReceive(handler)](https://playwright.dev/docs/api/class-websocketroute#web-socket-route-route-receive)
|
||||
* is called.
|
||||
* - Messages sent by the `WebSocket.send()` call in the page will be automatically sent to the server, unless
|
||||
* [webSocketRoute.routeSend(handler)](https://playwright.dev/docs/api/class-websocketroute#web-socket-route-route-send)
|
||||
* is called.
|
||||
*/
|
||||
connect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Dispatches a message to the [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object in the
|
||||
* page, like it was received from the server.
|
||||
* @param message Message to receive.
|
||||
*/
|
||||
receive(message: string|Buffer): void;
|
||||
|
||||
/**
|
||||
* Sends a message to the server, like it was sent in the page with `WebSocket.send()`.
|
||||
* @param message Message to send.
|
||||
*/
|
||||
send(message: string|Buffer): void;
|
||||
|
||||
/**
|
||||
* URL of the WebSocket created in the page.
|
||||
*/
|
||||
url(): string;
|
||||
|
||||
[Symbol.asyncDispose](): Promise<void>;
|
||||
}
|
||||
|
||||
type DeviceDescriptor = {
|
||||
viewport: ViewportSize;
|
||||
userAgent: string;
|
||||
|
|
@ -18240,11 +18424,12 @@ export interface FileChooser {
|
|||
/**
|
||||
* FrameLocator represents a view to the `iframe` on the page. It captures the logic sufficient to retrieve the
|
||||
* `iframe` and locate elements in that iframe. FrameLocator can be created with either
|
||||
* [locator.contentFrame()](https://playwright.dev/docs/api/class-locator#locator-content-frame),
|
||||
* [page.frameLocator(selector)](https://playwright.dev/docs/api/class-page#page-frame-locator) or
|
||||
* [locator.frameLocator(selector)](https://playwright.dev/docs/api/class-locator#locator-frame-locator) method.
|
||||
*
|
||||
* ```js
|
||||
* const locator = page.frameLocator('#my-frame').getByText('Submit');
|
||||
* const locator = page.locator('#my-frame').contentFrame().getByText('Submit');
|
||||
* await locator.click();
|
||||
* ```
|
||||
*
|
||||
|
|
@ -18255,10 +18440,10 @@ export interface FileChooser {
|
|||
*
|
||||
* ```js
|
||||
* // Throws if there are several frames in DOM:
|
||||
* await page.frameLocator('.result-frame').getByRole('button').click();
|
||||
* await page.locator('.result-frame').contentFrame().getByRole('button').click();
|
||||
*
|
||||
* // Works because we explicitly tell locator to pick the first frame:
|
||||
* await page.frameLocator('.result-frame').first().getByRole('button').click();
|
||||
* await page.locator('.result-frame').contentFrame().first().getByRole('button').click();
|
||||
* ```
|
||||
*
|
||||
* **Converting Locator to FrameLocator**
|
||||
|
|
@ -18274,6 +18459,8 @@ export interface FileChooser {
|
|||
export interface FrameLocator {
|
||||
/**
|
||||
* Returns locator to the first matching frame.
|
||||
* @deprecated Use [locator.first()](https://playwright.dev/docs/api/class-locator#locator-first) followed by
|
||||
* [locator.contentFrame()](https://playwright.dev/docs/api/class-locator#locator-content-frame) instead.
|
||||
*/
|
||||
first(): FrameLocator;
|
||||
|
||||
|
|
@ -18598,6 +18785,8 @@ export interface FrameLocator {
|
|||
|
||||
/**
|
||||
* Returns locator to the last matching frame.
|
||||
* @deprecated Use [locator.last()](https://playwright.dev/docs/api/class-locator#locator-last) followed by
|
||||
* [locator.contentFrame()](https://playwright.dev/docs/api/class-locator#locator-content-frame) instead.
|
||||
*/
|
||||
last(): FrameLocator;
|
||||
|
||||
|
|
@ -18650,6 +18839,8 @@ export interface FrameLocator {
|
|||
|
||||
/**
|
||||
* Returns locator to the n-th matching frame. It's zero based, `nth(0)` selects the first frame.
|
||||
* @deprecated Use [locator.nth(index)](https://playwright.dev/docs/api/class-locator#locator-nth) followed by
|
||||
* [locator.contentFrame()](https://playwright.dev/docs/api/class-locator#locator-content-frame) instead.
|
||||
* @param index
|
||||
*/
|
||||
nth(index: number): FrameLocator;
|
||||
|
|
@ -18666,7 +18857,7 @@ export interface FrameLocator {
|
|||
* **Usage**
|
||||
*
|
||||
* ```js
|
||||
* const frameLocator = page.frameLocator('iframe[name="embedded"]');
|
||||
* const frameLocator = page.locator('iframe[name="embedded"]').contentFrame();
|
||||
* // ...
|
||||
* const locator = frameLocator.owner();
|
||||
* await expect(locator).toBeVisible();
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ function babelTransformOptions(isTypeScript: boolean, isModule: boolean, plugins
|
|||
[require('@babel/plugin-syntax-async-generators')],
|
||||
[require('@babel/plugin-syntax-object-rest-spread')],
|
||||
[require('@babel/plugin-transform-export-namespace-from')],
|
||||
[require('@babel/plugin-syntax-import-attributes'), { deprecatedAssertSyntax: true }],
|
||||
[
|
||||
// From https://github.com/G-Rath/babel-plugin-replace-ts-export-assignment/blob/8dfdca32c8aa428574b0cae341444fc5822f2dc6/src/index.ts
|
||||
(
|
||||
|
|
@ -86,8 +87,6 @@ function babelTransformOptions(isTypeScript: boolean, isModule: boolean, plugins
|
|||
}
|
||||
})
|
||||
]);
|
||||
} else {
|
||||
plugins.push([require('@babel/plugin-syntax-import-attributes'), { deprecatedAssertSyntax: true }]);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ class HtmlReporter implements ReporterV2 {
|
|||
const shouldOpen = !this._options._isTestServer && (this._open === 'always' || (!ok && this._open === 'on-failure'));
|
||||
if (shouldOpen) {
|
||||
await showHTMLReport(this._outputFolder, this._host, this._port, singleTestId);
|
||||
} else if (this._options._mode === 'test') {
|
||||
} else if (this._options._mode === 'test' && !this._options._isTestServer) {
|
||||
const packageManagerCommand = getPackageManagerExecCommand();
|
||||
const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? '' : ' ' + path.relative(process.cwd(), this._outputFolder);
|
||||
const hostArg = this._host ? ` --host ${this._host}` : '';
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ export class TestServerDispatcher implements TestServerInterface {
|
|||
constructor(configLocation: ConfigLocation) {
|
||||
this._configLocation = configLocation;
|
||||
this.transport = {
|
||||
onconnect: () => {},
|
||||
dispatch: (method, params) => (this as any)[method](params),
|
||||
onclose: () => {
|
||||
if (this._closeOnDisconnect)
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ export class TimeoutManager {
|
|||
|
||||
setIgnoreTimeouts() {
|
||||
this._ignoreTimeouts = true;
|
||||
if (this._running)
|
||||
this._updateTimeout(this._running);
|
||||
}
|
||||
|
||||
interrupt() {
|
||||
|
|
|
|||
20
packages/playwright/types/test.d.ts
vendored
20
packages/playwright/types/test.d.ts
vendored
|
|
@ -7788,6 +7788,26 @@ interface SnapshotAssertions {
|
|||
}): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a location in the source code where [TestCase] or [Suite] is defined.
|
||||
*/
|
||||
export interface Location {
|
||||
/**
|
||||
* Column number in the source file.
|
||||
*/
|
||||
column: number;
|
||||
|
||||
/**
|
||||
* Path to the source file.
|
||||
*/
|
||||
file: string;
|
||||
|
||||
/**
|
||||
* Line number in the source file.
|
||||
*/
|
||||
line: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* `TestInfo` contains information about currently running test. It is available to test functions,
|
||||
* [test.beforeEach([title, hookFunction])](https://playwright.dev/docs/api/class-test#test-before-each),
|
||||
|
|
|
|||
24
packages/playwright/types/testReporter.d.ts
vendored
24
packages/playwright/types/testReporter.d.ts
vendored
|
|
@ -15,8 +15,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { TestStatus, Metadata, PlaywrightTestOptions, PlaywrightWorkerOptions, ReporterDescription, FullConfig, FullProject } from './test';
|
||||
export type { FullConfig, FullProject, TestStatus } from './test';
|
||||
import type { TestStatus, Metadata, PlaywrightTestOptions, PlaywrightWorkerOptions, ReporterDescription, FullConfig, FullProject, Location } from './test';
|
||||
export type { FullConfig, FullProject, TestStatus, Location } from './test';
|
||||
|
||||
/**
|
||||
* Result of the full test run.
|
||||
|
|
@ -319,26 +319,6 @@ export type JSONReportSTDIOEntry = { text: string } | { buffer: string };
|
|||
export {};
|
||||
|
||||
|
||||
/**
|
||||
* Represents a location in the source code where {@link TestCase} or {@link Suite} is defined.
|
||||
*/
|
||||
export interface Location {
|
||||
/**
|
||||
* Column number in the source file.
|
||||
*/
|
||||
column: number;
|
||||
|
||||
/**
|
||||
* Path to the source file.
|
||||
*/
|
||||
file: string;
|
||||
|
||||
/**
|
||||
* Line number in the source file.
|
||||
*/
|
||||
line: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* `Suite` is a group of tests. All tests in Playwright Test form the following hierarchy:
|
||||
* - Root suite has a child suite for each {@link FullProject}.
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export type InitializerTraits<T> =
|
|||
T extends BindingCallChannel ? BindingCallInitializer :
|
||||
T extends WebSocketChannel ? WebSocketInitializer :
|
||||
T extends ResponseChannel ? ResponseInitializer :
|
||||
T extends WebSocketRouteChannel ? WebSocketRouteInitializer :
|
||||
T extends RouteChannel ? RouteInitializer :
|
||||
T extends RequestChannel ? RequestInitializer :
|
||||
T extends ElementHandleChannel ? ElementHandleInitializer :
|
||||
|
|
@ -77,6 +78,7 @@ export type EventsTraits<T> =
|
|||
T extends BindingCallChannel ? BindingCallEvents :
|
||||
T extends WebSocketChannel ? WebSocketEvents :
|
||||
T extends ResponseChannel ? ResponseEvents :
|
||||
T extends WebSocketRouteChannel ? WebSocketRouteEvents :
|
||||
T extends RouteChannel ? RouteEvents :
|
||||
T extends RequestChannel ? RequestEvents :
|
||||
T extends ElementHandleChannel ? ElementHandleEvents :
|
||||
|
|
@ -114,6 +116,7 @@ export type EventTargetTraits<T> =
|
|||
T extends BindingCallChannel ? BindingCallEventTarget :
|
||||
T extends WebSocketChannel ? WebSocketEventTarget :
|
||||
T extends ResponseChannel ? ResponseEventTarget :
|
||||
T extends WebSocketRouteChannel ? WebSocketRouteEventTarget :
|
||||
T extends RouteChannel ? RouteEventTarget :
|
||||
T extends RequestChannel ? RequestEventTarget :
|
||||
T extends ElementHandleChannel ? ElementHandleEventTarget :
|
||||
|
|
@ -1493,6 +1496,7 @@ export interface BrowserContextEventTarget {
|
|||
on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this;
|
||||
on(event: 'pageError', callback: (params: BrowserContextPageErrorEvent) => void): this;
|
||||
on(event: 'route', callback: (params: BrowserContextRouteEvent) => void): this;
|
||||
on(event: 'webSocketRoute', callback: (params: BrowserContextWebSocketRouteEvent) => void): this;
|
||||
on(event: 'video', callback: (params: BrowserContextVideoEvent) => void): this;
|
||||
on(event: 'backgroundPage', callback: (params: BrowserContextBackgroundPageEvent) => void): this;
|
||||
on(event: 'serviceWorker', callback: (params: BrowserContextServiceWorkerEvent) => void): this;
|
||||
|
|
@ -1518,10 +1522,11 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
|
|||
setGeolocation(params: BrowserContextSetGeolocationParams, metadata?: CallMetadata): Promise<BrowserContextSetGeolocationResult>;
|
||||
setHTTPCredentials(params: BrowserContextSetHTTPCredentialsParams, metadata?: CallMetadata): Promise<BrowserContextSetHTTPCredentialsResult>;
|
||||
setNetworkInterceptionPatterns(params: BrowserContextSetNetworkInterceptionPatternsParams, metadata?: CallMetadata): Promise<BrowserContextSetNetworkInterceptionPatternsResult>;
|
||||
setWebSocketInterceptionPatterns(params: BrowserContextSetWebSocketInterceptionPatternsParams, metadata?: CallMetadata): Promise<BrowserContextSetWebSocketInterceptionPatternsResult>;
|
||||
setOffline(params: BrowserContextSetOfflineParams, metadata?: CallMetadata): Promise<BrowserContextSetOfflineResult>;
|
||||
storageState(params?: BrowserContextStorageStateParams, metadata?: CallMetadata): Promise<BrowserContextStorageStateResult>;
|
||||
pause(params?: BrowserContextPauseParams, metadata?: CallMetadata): Promise<BrowserContextPauseResult>;
|
||||
recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: CallMetadata): Promise<BrowserContextRecorderSupplementEnableResult>;
|
||||
enableRecorder(params: BrowserContextEnableRecorderParams, metadata?: CallMetadata): Promise<BrowserContextEnableRecorderResult>;
|
||||
newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: CallMetadata): Promise<BrowserContextNewCDPSessionResult>;
|
||||
harStart(params: BrowserContextHarStartParams, metadata?: CallMetadata): Promise<BrowserContextHarStartResult>;
|
||||
harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>;
|
||||
|
|
@ -1563,6 +1568,9 @@ export type BrowserContextPageErrorEvent = {
|
|||
export type BrowserContextRouteEvent = {
|
||||
route: RouteChannel,
|
||||
};
|
||||
export type BrowserContextWebSocketRouteEvent = {
|
||||
webSocketRoute: WebSocketRouteChannel,
|
||||
};
|
||||
export type BrowserContextVideoEvent = {
|
||||
artifact: ArtifactChannel,
|
||||
};
|
||||
|
|
@ -1731,6 +1739,17 @@ export type BrowserContextSetNetworkInterceptionPatternsOptions = {
|
|||
|
||||
};
|
||||
export type BrowserContextSetNetworkInterceptionPatternsResult = void;
|
||||
export type BrowserContextSetWebSocketInterceptionPatternsParams = {
|
||||
patterns: {
|
||||
glob?: string,
|
||||
regexSource?: string,
|
||||
regexFlags?: string,
|
||||
}[],
|
||||
};
|
||||
export type BrowserContextSetWebSocketInterceptionPatternsOptions = {
|
||||
|
||||
};
|
||||
export type BrowserContextSetWebSocketInterceptionPatternsResult = void;
|
||||
export type BrowserContextSetOfflineParams = {
|
||||
offline: boolean,
|
||||
};
|
||||
|
|
@ -1747,9 +1766,10 @@ export type BrowserContextStorageStateResult = {
|
|||
export type BrowserContextPauseParams = {};
|
||||
export type BrowserContextPauseOptions = {};
|
||||
export type BrowserContextPauseResult = void;
|
||||
export type BrowserContextRecorderSupplementEnableParams = {
|
||||
export type BrowserContextEnableRecorderParams = {
|
||||
language?: string,
|
||||
mode?: 'inspecting' | 'recording',
|
||||
codegenMode?: 'actions' | 'trace-events',
|
||||
pauseOnNextStatement?: boolean,
|
||||
testIdAttributeName?: string,
|
||||
launchOptions?: any,
|
||||
|
|
@ -1759,9 +1779,10 @@ export type BrowserContextRecorderSupplementEnableParams = {
|
|||
outputFile?: string,
|
||||
omitCallTracking?: boolean,
|
||||
};
|
||||
export type BrowserContextRecorderSupplementEnableOptions = {
|
||||
export type BrowserContextEnableRecorderOptions = {
|
||||
language?: string,
|
||||
mode?: 'inspecting' | 'recording',
|
||||
codegenMode?: 'actions' | 'trace-events',
|
||||
pauseOnNextStatement?: boolean,
|
||||
testIdAttributeName?: string,
|
||||
launchOptions?: any,
|
||||
|
|
@ -1771,7 +1792,7 @@ export type BrowserContextRecorderSupplementEnableOptions = {
|
|||
outputFile?: string,
|
||||
omitCallTracking?: boolean,
|
||||
};
|
||||
export type BrowserContextRecorderSupplementEnableResult = void;
|
||||
export type BrowserContextEnableRecorderResult = void;
|
||||
export type BrowserContextNewCDPSessionParams = {
|
||||
page?: PageChannel,
|
||||
frame?: FrameChannel,
|
||||
|
|
@ -1890,6 +1911,7 @@ export interface BrowserContextEvents {
|
|||
'page': BrowserContextPageEvent;
|
||||
'pageError': BrowserContextPageErrorEvent;
|
||||
'route': BrowserContextRouteEvent;
|
||||
'webSocketRoute': BrowserContextWebSocketRouteEvent;
|
||||
'video': BrowserContextVideoEvent;
|
||||
'backgroundPage': BrowserContextBackgroundPageEvent;
|
||||
'serviceWorker': BrowserContextServiceWorkerEvent;
|
||||
|
|
@ -1919,6 +1941,7 @@ export interface PageEventTarget {
|
|||
on(event: 'frameDetached', callback: (params: PageFrameDetachedEvent) => void): this;
|
||||
on(event: 'locatorHandlerTriggered', callback: (params: PageLocatorHandlerTriggeredEvent) => void): this;
|
||||
on(event: 'route', callback: (params: PageRouteEvent) => void): this;
|
||||
on(event: 'webSocketRoute', callback: (params: PageWebSocketRouteEvent) => void): this;
|
||||
on(event: 'video', callback: (params: PageVideoEvent) => void): this;
|
||||
on(event: 'webSocket', callback: (params: PageWebSocketEvent) => void): this;
|
||||
on(event: 'worker', callback: (params: PageWorkerEvent) => void): this;
|
||||
|
|
@ -1942,6 +1965,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
|
|||
screenshot(params: PageScreenshotParams, metadata?: CallMetadata): Promise<PageScreenshotResult>;
|
||||
setExtraHTTPHeaders(params: PageSetExtraHTTPHeadersParams, metadata?: CallMetadata): Promise<PageSetExtraHTTPHeadersResult>;
|
||||
setNetworkInterceptionPatterns(params: PageSetNetworkInterceptionPatternsParams, metadata?: CallMetadata): Promise<PageSetNetworkInterceptionPatternsResult>;
|
||||
setWebSocketInterceptionPatterns(params: PageSetWebSocketInterceptionPatternsParams, metadata?: CallMetadata): Promise<PageSetWebSocketInterceptionPatternsResult>;
|
||||
setViewportSize(params: PageSetViewportSizeParams, metadata?: CallMetadata): Promise<PageSetViewportSizeResult>;
|
||||
keyboardDown(params: PageKeyboardDownParams, metadata?: CallMetadata): Promise<PageKeyboardDownResult>;
|
||||
keyboardUp(params: PageKeyboardUpParams, metadata?: CallMetadata): Promise<PageKeyboardUpResult>;
|
||||
|
|
@ -1989,6 +2013,9 @@ export type PageLocatorHandlerTriggeredEvent = {
|
|||
export type PageRouteEvent = {
|
||||
route: RouteChannel,
|
||||
};
|
||||
export type PageWebSocketRouteEvent = {
|
||||
webSocketRoute: WebSocketRouteChannel,
|
||||
};
|
||||
export type PageVideoEvent = {
|
||||
artifact: ArtifactChannel,
|
||||
};
|
||||
|
|
@ -2221,6 +2248,17 @@ export type PageSetNetworkInterceptionPatternsOptions = {
|
|||
|
||||
};
|
||||
export type PageSetNetworkInterceptionPatternsResult = void;
|
||||
export type PageSetWebSocketInterceptionPatternsParams = {
|
||||
patterns: {
|
||||
glob?: string,
|
||||
regexSource?: string,
|
||||
regexFlags?: string,
|
||||
}[],
|
||||
};
|
||||
export type PageSetWebSocketInterceptionPatternsOptions = {
|
||||
|
||||
};
|
||||
export type PageSetWebSocketInterceptionPatternsResult = void;
|
||||
export type PageSetViewportSizeParams = {
|
||||
viewportSize: {
|
||||
width: number,
|
||||
|
|
@ -2448,6 +2486,7 @@ export interface PageEvents {
|
|||
'frameDetached': PageFrameDetachedEvent;
|
||||
'locatorHandlerTriggered': PageLocatorHandlerTriggeredEvent;
|
||||
'route': PageRouteEvent;
|
||||
'webSocketRoute': PageWebSocketRouteEvent;
|
||||
'video': PageVideoEvent;
|
||||
'webSocket': PageWebSocketEvent;
|
||||
'worker': PageWorkerEvent;
|
||||
|
|
@ -3732,7 +3771,6 @@ export type RouteRedirectNavigationRequestOptions = {
|
|||
export type RouteRedirectNavigationRequestResult = void;
|
||||
export type RouteAbortParams = {
|
||||
errorCode?: string,
|
||||
requestUrl: string,
|
||||
};
|
||||
export type RouteAbortOptions = {
|
||||
errorCode?: string,
|
||||
|
|
@ -3743,7 +3781,6 @@ export type RouteContinueParams = {
|
|||
method?: string,
|
||||
headers?: NameValue[],
|
||||
postData?: Binary,
|
||||
requestUrl: string,
|
||||
isFallback: boolean,
|
||||
};
|
||||
export type RouteContinueOptions = {
|
||||
|
|
@ -3759,7 +3796,6 @@ export type RouteFulfillParams = {
|
|||
body?: string,
|
||||
isBase64?: boolean,
|
||||
fetchResponseUid?: string,
|
||||
requestUrl: string,
|
||||
};
|
||||
export type RouteFulfillOptions = {
|
||||
status?: number,
|
||||
|
|
@ -3773,6 +3809,70 @@ export type RouteFulfillResult = void;
|
|||
export interface RouteEvents {
|
||||
}
|
||||
|
||||
// ----------- WebSocketRoute -----------
|
||||
export type WebSocketRouteInitializer = {
|
||||
url: string,
|
||||
};
|
||||
export interface WebSocketRouteEventTarget {
|
||||
on(event: 'messageFromPage', callback: (params: WebSocketRouteMessageFromPageEvent) => void): this;
|
||||
on(event: 'messageFromServer', callback: (params: WebSocketRouteMessageFromServerEvent) => void): this;
|
||||
on(event: 'close', callback: (params: WebSocketRouteCloseEvent) => void): this;
|
||||
}
|
||||
export interface WebSocketRouteChannel extends WebSocketRouteEventTarget, Channel {
|
||||
_type_WebSocketRoute: boolean;
|
||||
connect(params?: WebSocketRouteConnectParams, metadata?: CallMetadata): Promise<WebSocketRouteConnectResult>;
|
||||
ensureOpened(params?: WebSocketRouteEnsureOpenedParams, metadata?: CallMetadata): Promise<WebSocketRouteEnsureOpenedResult>;
|
||||
sendToPage(params: WebSocketRouteSendToPageParams, metadata?: CallMetadata): Promise<WebSocketRouteSendToPageResult>;
|
||||
sendToServer(params: WebSocketRouteSendToServerParams, metadata?: CallMetadata): Promise<WebSocketRouteSendToServerResult>;
|
||||
close(params: WebSocketRouteCloseParams, metadata?: CallMetadata): Promise<WebSocketRouteCloseResult>;
|
||||
}
|
||||
export type WebSocketRouteMessageFromPageEvent = {
|
||||
message: string,
|
||||
isBase64: boolean,
|
||||
};
|
||||
export type WebSocketRouteMessageFromServerEvent = {
|
||||
message: string,
|
||||
isBase64: boolean,
|
||||
};
|
||||
export type WebSocketRouteCloseEvent = {};
|
||||
export type WebSocketRouteConnectParams = {};
|
||||
export type WebSocketRouteConnectOptions = {};
|
||||
export type WebSocketRouteConnectResult = void;
|
||||
export type WebSocketRouteEnsureOpenedParams = {};
|
||||
export type WebSocketRouteEnsureOpenedOptions = {};
|
||||
export type WebSocketRouteEnsureOpenedResult = void;
|
||||
export type WebSocketRouteSendToPageParams = {
|
||||
message: string,
|
||||
isBase64: boolean,
|
||||
};
|
||||
export type WebSocketRouteSendToPageOptions = {
|
||||
|
||||
};
|
||||
export type WebSocketRouteSendToPageResult = void;
|
||||
export type WebSocketRouteSendToServerParams = {
|
||||
message: string,
|
||||
isBase64: boolean,
|
||||
};
|
||||
export type WebSocketRouteSendToServerOptions = {
|
||||
|
||||
};
|
||||
export type WebSocketRouteSendToServerResult = void;
|
||||
export type WebSocketRouteCloseParams = {
|
||||
code?: number,
|
||||
reason?: string,
|
||||
};
|
||||
export type WebSocketRouteCloseOptions = {
|
||||
code?: number,
|
||||
reason?: string,
|
||||
};
|
||||
export type WebSocketRouteCloseResult = void;
|
||||
|
||||
export interface WebSocketRouteEvents {
|
||||
'messageFromPage': WebSocketRouteMessageFromPageEvent;
|
||||
'messageFromServer': WebSocketRouteMessageFromServerEvent;
|
||||
'close': WebSocketRouteCloseEvent;
|
||||
}
|
||||
|
||||
export type ResourceTiming = {
|
||||
startTime: number,
|
||||
domainLookupStart: number,
|
||||
|
|
|
|||
|
|
@ -1160,6 +1160,17 @@ BrowserContext:
|
|||
regexSource: string?
|
||||
regexFlags: string?
|
||||
|
||||
setWebSocketInterceptionPatterns:
|
||||
parameters:
|
||||
patterns:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
glob: string?
|
||||
regexSource: string?
|
||||
regexFlags: string?
|
||||
|
||||
setOffline:
|
||||
parameters:
|
||||
offline: boolean
|
||||
|
|
@ -1176,7 +1187,7 @@ BrowserContext:
|
|||
pause:
|
||||
experimental: True
|
||||
|
||||
recorderSupplementEnable:
|
||||
enableRecorder:
|
||||
experimental: True
|
||||
parameters:
|
||||
language: string?
|
||||
|
|
@ -1185,6 +1196,11 @@ BrowserContext:
|
|||
literals:
|
||||
- inspecting
|
||||
- recording
|
||||
codegenMode:
|
||||
type: enum?
|
||||
literals:
|
||||
- actions
|
||||
- trace-events
|
||||
pauseOnNextStatement: boolean?
|
||||
testIdAttributeName: string?
|
||||
launchOptions: json?
|
||||
|
|
@ -1305,6 +1321,10 @@ BrowserContext:
|
|||
parameters:
|
||||
route: Route
|
||||
|
||||
webSocketRoute:
|
||||
parameters:
|
||||
webSocketRoute: WebSocketRoute
|
||||
|
||||
video:
|
||||
parameters:
|
||||
artifact: Artifact
|
||||
|
|
@ -1520,6 +1540,17 @@ Page:
|
|||
regexSource: string?
|
||||
regexFlags: string?
|
||||
|
||||
setWebSocketInterceptionPatterns:
|
||||
parameters:
|
||||
patterns:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
glob: string?
|
||||
regexSource: string?
|
||||
regexFlags: string?
|
||||
|
||||
setViewportSize:
|
||||
parameters:
|
||||
viewportSize:
|
||||
|
|
@ -1771,6 +1802,10 @@ Page:
|
|||
parameters:
|
||||
route: Route
|
||||
|
||||
webSocketRoute:
|
||||
parameters:
|
||||
webSocketRoute: WebSocketRoute
|
||||
|
||||
video:
|
||||
parameters:
|
||||
artifact: Artifact
|
||||
|
|
@ -2911,7 +2946,6 @@ Route:
|
|||
abort:
|
||||
parameters:
|
||||
errorCode: string?
|
||||
requestUrl: string
|
||||
|
||||
continue:
|
||||
parameters:
|
||||
|
|
@ -2921,7 +2955,6 @@ Route:
|
|||
type: array?
|
||||
items: NameValue
|
||||
postData: binary?
|
||||
requestUrl: string
|
||||
isFallback: boolean
|
||||
|
||||
fulfill:
|
||||
|
|
@ -2934,7 +2967,49 @@ Route:
|
|||
body: string?
|
||||
isBase64: boolean?
|
||||
fetchResponseUid: string?
|
||||
requestUrl: string
|
||||
|
||||
|
||||
WebSocketRoute:
|
||||
type: interface
|
||||
|
||||
initializer:
|
||||
url: string
|
||||
|
||||
commands:
|
||||
|
||||
connect:
|
||||
|
||||
ensureOpened:
|
||||
|
||||
sendToPage:
|
||||
parameters:
|
||||
message: string
|
||||
isBase64: boolean
|
||||
|
||||
sendToServer:
|
||||
parameters:
|
||||
message: string
|
||||
isBase64: boolean
|
||||
|
||||
close:
|
||||
parameters:
|
||||
code: number?
|
||||
reason: string?
|
||||
|
||||
events:
|
||||
|
||||
messageFromPage:
|
||||
parameters:
|
||||
message: string
|
||||
isBase64: boolean
|
||||
|
||||
messageFromServer:
|
||||
parameters:
|
||||
message: string
|
||||
isBase64: boolean
|
||||
|
||||
close:
|
||||
|
||||
|
||||
ResourceTiming:
|
||||
type: object
|
||||
|
|
|
|||
|
|
@ -27,7 +27,10 @@ export const Main: React.FC = ({
|
|||
const [mode, setMode] = React.useState<Mode>('none');
|
||||
|
||||
window.playwrightSetMode = setMode;
|
||||
window.playwrightSetSources = setSources;
|
||||
window.playwrightSetSources = React.useCallback((sources: Source[]) => {
|
||||
setSources(sources);
|
||||
window.playwrightSourcesEchoForTest = sources;
|
||||
}, []);
|
||||
window.playwrightSetPaused = setPaused;
|
||||
window.playwrightUpdateLogs = callLogs => {
|
||||
setLog(log => {
|
||||
|
|
@ -40,6 +43,5 @@ export const Main: React.FC = ({
|
|||
});
|
||||
};
|
||||
|
||||
window.playwrightSourcesEchoForTest = sources;
|
||||
return <Recorder sources={sources} paused={paused} log={log} mode={mode} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,12 +47,14 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, clientI
|
|||
}
|
||||
set.add(traceUrl);
|
||||
|
||||
const isRecorderMode = traceUrl.includes('/playwright-recorder-trace-');
|
||||
|
||||
const traceModel = new TraceModel();
|
||||
try {
|
||||
// Allow 10% to hop from sw to page.
|
||||
const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]);
|
||||
const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl) : new ZipTraceModelBackend(traceUrl, fetchProgress);
|
||||
await traceModel.load(backend, unzipProgress);
|
||||
await traceModel.load(backend, isRecorderMode, unzipProgress);
|
||||
} catch (error: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
|
|
|
|||
285
packages/trace-viewer/src/third_party/devtools.ts
vendored
Normal file
285
packages/trace-viewer/src/third_party/devtools.ts
vendored
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
// Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
// Modifications copyright (c) Microsoft Corporation.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
/*
|
||||
* Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
|
||||
* Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org>
|
||||
* Copyright (C) 2011 Google Inc. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
|
||||
* its contributors may be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import type { Entry } from '@trace/har';
|
||||
|
||||
// The following function is derived from Chromium's source code
|
||||
// https://github.com/ChromeDevTools/devtools-frontend/blob/83cbe41b4107e188a1f66fdf6ea3a9cca42587c6/front_end/panels/network/NetworkLogView.ts#L2363
|
||||
export async function generateCurlCommand(resource: Entry): Promise<string> {
|
||||
const platform = navigator.platform.includes('Win') ? 'win' : 'unix';
|
||||
let command: string[] = [];
|
||||
// Most of these headers are derived from the URL and are automatically added by cURL.
|
||||
// The |Accept-Encoding| header is ignored to prevent decompression errors. crbug.com/1015321
|
||||
const ignoredHeaders =
|
||||
new Set<string>(['accept-encoding', 'host', 'method', 'path', 'scheme', 'version', 'authority', 'protocol']);
|
||||
|
||||
function escapeStringWin(str: string): string {
|
||||
/* Always escape the " characters so that we can use caret escaping.
|
||||
|
||||
Because cmd.exe parser and MS Crt arguments parsers use some of the
|
||||
same escape characters, they can interact with each other in
|
||||
horrible ways, the order of operations is critical.
|
||||
|
||||
Replace \ with \\ first because it is an escape character for certain
|
||||
conditions in both parsers.
|
||||
|
||||
Replace all " with \" to ensure the first parser does not remove it.
|
||||
|
||||
Then escape all characters we are not sure about with ^ to ensure it
|
||||
gets to MS Crt parser safely.
|
||||
|
||||
The % character is special because MS Crt parser will try and look for
|
||||
ENV variables and fill them in its place. We cannot escape them with %
|
||||
and cannot escape them with ^ (because it's cmd.exe's escape not MS Crt
|
||||
parser); So we can get cmd.exe parser to escape the character after it,
|
||||
if it is followed by a valid beginning character of an ENV variable.
|
||||
This ensures we do not try and double escape another ^ if it was placed
|
||||
by the previous replace.
|
||||
|
||||
Lastly we replace new lines with ^ and TWO new lines because the first
|
||||
new line is there to enact the escape command the second is the character
|
||||
to escape (in this case new line).
|
||||
*/
|
||||
const encapsChars = '^"';
|
||||
return encapsChars +
|
||||
str.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/[^a-zA-Z0-9\s_\-:=+~'\/.',?;()*`]/g, '^$&')
|
||||
.replace(/%(?=[a-zA-Z0-9_])/g, '%^')
|
||||
.replace(/\r?\n/g, '^\n\n') +
|
||||
encapsChars;
|
||||
}
|
||||
|
||||
function escapeStringPosix(str: string): string {
|
||||
function escapeCharacter(x: string): string {
|
||||
const code = x.charCodeAt(0);
|
||||
let hexString = code.toString(16);
|
||||
// Zero pad to four digits to comply with ANSI-C Quoting:
|
||||
// http://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
|
||||
while (hexString.length < 4)
|
||||
hexString = '0' + hexString;
|
||||
|
||||
|
||||
return '\\u' + hexString;
|
||||
}
|
||||
|
||||
if (/[\0-\x1F\x7F-\x9F!]|\'/.test(str)) {
|
||||
// Use ANSI-C quoting syntax.
|
||||
return '$\'' +
|
||||
str.replace(/\\/g, '\\\\')
|
||||
.replace(/\'/g, '\\\'')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/[\0-\x1F\x7F-\x9F!]/g, escapeCharacter) +
|
||||
'\'';
|
||||
}
|
||||
// Use single quote syntax.
|
||||
return '\'' + str + '\'';
|
||||
}
|
||||
|
||||
// cURL command expected to run on the same platform that DevTools run
|
||||
// (it may be different from the inspected page platform).
|
||||
const escapeString = platform === 'win' ? escapeStringWin : escapeStringPosix;
|
||||
|
||||
command.push(escapeString(resource.request.url).replace(/[[{}\]]/g, '\\$&'));
|
||||
|
||||
let inferredMethod = 'GET';
|
||||
const data = [];
|
||||
const formData = await fetchRequestPostData(resource);
|
||||
if (formData) {
|
||||
// Note that formData is not necessarily urlencoded because it might for example
|
||||
// come from a fetch request made with an explicitly unencoded body.
|
||||
data.push('--data-raw ' + escapeString(formData));
|
||||
ignoredHeaders.add('content-length');
|
||||
inferredMethod = 'POST';
|
||||
}
|
||||
|
||||
if (resource.request.method !== inferredMethod)
|
||||
command.push('-X ' + escapeString(resource.request.method));
|
||||
|
||||
|
||||
const requestHeaders = resource.request.headers;
|
||||
for (let i = 0; i < requestHeaders.length; i++) {
|
||||
const header = requestHeaders[i];
|
||||
const name = header.name.replace(/^:/, ''); // Translate SPDY v3 headers to HTTP headers.
|
||||
if (ignoredHeaders.has(name.toLowerCase()))
|
||||
continue;
|
||||
|
||||
if (header.value.trim()) {
|
||||
command.push('-H ' + escapeString(name + ': ' + header.value));
|
||||
} else {
|
||||
// A header passed with -H with no value or only whitespace as its
|
||||
// value tells curl to not set the header at all. To post an empty
|
||||
// header, you have to terminate it with a semicolon.
|
||||
command.push('-H ' + escapeString(name + ';'));
|
||||
}
|
||||
}
|
||||
command = command.concat(data);
|
||||
|
||||
return 'curl ' + command.join(command.length >= 3 ? (platform === 'win' ? ' ^\n ' : ' \\\n ') : ' ');
|
||||
}
|
||||
|
||||
const enum FetchStyle {
|
||||
BROWSER = 0,
|
||||
NODE_JS = 1,
|
||||
}
|
||||
|
||||
export async function generateFetchCall(resource: Entry, style: FetchStyle = FetchStyle.BROWSER): Promise<string> {
|
||||
const ignoredHeaders = new Set<string>([
|
||||
// Internal headers
|
||||
'method',
|
||||
'path',
|
||||
'scheme',
|
||||
'version',
|
||||
|
||||
// Unsafe headers
|
||||
// Keep this list synchronized with src/net/http/http_util.cc
|
||||
'accept-charset',
|
||||
'accept-encoding',
|
||||
'access-control-request-headers',
|
||||
'access-control-request-method',
|
||||
'connection',
|
||||
'content-length',
|
||||
'cookie',
|
||||
'cookie2',
|
||||
'date',
|
||||
'dnt',
|
||||
'expect',
|
||||
'host',
|
||||
'keep-alive',
|
||||
'origin',
|
||||
'referer',
|
||||
'te',
|
||||
'trailer',
|
||||
'transfer-encoding',
|
||||
'upgrade',
|
||||
'via',
|
||||
// TODO(phistuck) - remove this once crbug.com/571722 is fixed.
|
||||
'user-agent',
|
||||
]);
|
||||
|
||||
const credentialHeaders = new Set<string>(['cookie', 'authorization']);
|
||||
|
||||
const url = JSON.stringify(resource.request.url);
|
||||
|
||||
const requestHeaders = resource.request.headers;
|
||||
const headerData: Headers = requestHeaders.reduce((result, header) => {
|
||||
const name = header.name;
|
||||
|
||||
if (!ignoredHeaders.has(name.toLowerCase()) && !name.includes(':'))
|
||||
result.append(name, header.value);
|
||||
|
||||
|
||||
return result;
|
||||
}, new Headers());
|
||||
|
||||
const headers: HeadersInit = {};
|
||||
for (const headerArray of headerData)
|
||||
headers[headerArray[0]] = headerArray[1];
|
||||
|
||||
|
||||
const credentials = resource.request.cookies.length ||
|
||||
requestHeaders.some(({ name }) => credentialHeaders.has(name.toLowerCase())) ?
|
||||
'include' :
|
||||
'omit';
|
||||
|
||||
const referrerHeader = requestHeaders.find(({ name }) => name.toLowerCase() === 'referer');
|
||||
|
||||
const referrer = referrerHeader ? referrerHeader.value : void 0;
|
||||
|
||||
const requestBody = await fetchRequestPostData(resource);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: Object.keys(headers).length ? headers : void 0,
|
||||
referrer,
|
||||
body: requestBody,
|
||||
method: resource.request.method,
|
||||
mode: 'cors',
|
||||
};
|
||||
|
||||
if (style === FetchStyle.NODE_JS) {
|
||||
const cookieHeader = requestHeaders.find(header => header.name.toLowerCase() === 'cookie');
|
||||
const extraHeaders: HeadersInit = {};
|
||||
// According to https://www.npmjs.com/package/node-fetch#class-request the
|
||||
// following properties are not implemented in Node.js.
|
||||
delete fetchOptions.mode;
|
||||
if (cookieHeader)
|
||||
extraHeaders['cookie'] = cookieHeader.value;
|
||||
|
||||
if (referrer) {
|
||||
delete fetchOptions.referrer;
|
||||
extraHeaders['Referer'] = referrer;
|
||||
}
|
||||
if (Object.keys(extraHeaders).length) {
|
||||
fetchOptions.headers = {
|
||||
...headers,
|
||||
...extraHeaders,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
fetchOptions.credentials = credentials;
|
||||
}
|
||||
|
||||
const options = JSON.stringify(fetchOptions, null, 2);
|
||||
return `fetch(${url}, ${options});`;
|
||||
}
|
||||
|
||||
async function fetchRequestPostData(resource: Entry) {
|
||||
return resource.request.postData?._sha1 ? await fetch(`sha1/${resource.request.postData._sha1}`).then(r => r.text()) : resource.request.postData?.text;
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import { parseClientSideCallMetadata } from '../../../packages/playwright-core/src/utils/isomorphic/traceUtils';
|
||||
import type { ContextEntry } from './entries';
|
||||
import type { ActionEntry, ContextEntry } from './entries';
|
||||
import { createEmptyContext } from './entries';
|
||||
import { SnapshotStorage } from './snapshotStorage';
|
||||
import { TraceModernizer } from './traceModernizer';
|
||||
|
|
@ -38,7 +38,7 @@ export class TraceModel {
|
|||
constructor() {
|
||||
}
|
||||
|
||||
async load(backend: TraceModelBackend, unzipProgress: (done: number, total: number) => void) {
|
||||
async load(backend: TraceModelBackend, isRecorderMode: boolean, unzipProgress: (done: number, total: number) => void) {
|
||||
this._backend = backend;
|
||||
|
||||
const ordinals: string[] = [];
|
||||
|
|
@ -72,7 +72,9 @@ export class TraceModel {
|
|||
modernizer.appendTrace(network);
|
||||
unzipProgress(++done, total);
|
||||
|
||||
contextEntry.actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime);
|
||||
const actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime);
|
||||
contextEntry.actions = isRecorderMode ? collapseActionsForRecorder(actions) : actions;
|
||||
|
||||
if (!backend.isLive()) {
|
||||
// Terminate actions w/o after event gracefully.
|
||||
// This would close after hooks event that has not been closed because
|
||||
|
|
@ -132,3 +134,19 @@ function stripEncodingFromContentType(contentType: string) {
|
|||
return charset[1];
|
||||
return contentType;
|
||||
}
|
||||
|
||||
function collapseActionsForRecorder(actions: ActionEntry[]): ActionEntry[] {
|
||||
const result: ActionEntry[] = [];
|
||||
for (const action of actions) {
|
||||
const lastAction = result[result.length - 1];
|
||||
const isSameAction = lastAction && lastAction.method === action.method && lastAction.pageId === action.pageId;
|
||||
const isSameSelector = lastAction && 'selector' in lastAction.params && 'selector' in action.params && action.params.selector === lastAction.params.selector;
|
||||
const shouldMerge = isSameAction && (action.method === 'goto' || (action.method === 'fill' && isSameSelector));
|
||||
if (!shouldMerge) {
|
||||
result.push(action);
|
||||
continue;
|
||||
}
|
||||
result[result.length - 1] = action;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,3 +6,4 @@
|
|||
../entries.ts
|
||||
../geometry.ts
|
||||
../../../playwright/src/isomorphic/**
|
||||
../third_party/devtools.ts
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@
|
|||
|
||||
.call-section {
|
||||
padding-left: 6px;
|
||||
padding-top: 2px;
|
||||
margin-top: 2px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
|
|
@ -53,9 +55,8 @@
|
|||
align-items: center;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
line-height: 18px;
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
max-height: 18px;
|
||||
}
|
||||
|
||||
.call-line:not(:hover) .toolbar-button.copy {
|
||||
|
|
@ -64,7 +65,8 @@
|
|||
|
||||
.call-line .toolbar-button.copy {
|
||||
margin-left: 5px;
|
||||
transform: scale(0.8);
|
||||
margin-top: -2px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.call-value {
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@ import * as React from 'react';
|
|||
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||
|
||||
export const CopyToClipboard: React.FunctionComponent<{
|
||||
value: string,
|
||||
value: string | (() => Promise<string>),
|
||||
description?: string,
|
||||
}> = ({ value, description }) => {
|
||||
const [icon, setIcon] = React.useState('copy');
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
const valuePromise = typeof value === 'function' ? value() : Promise.resolve(value);
|
||||
valuePromise.then(value => {
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setIcon('check');
|
||||
setTimeout(() => {
|
||||
|
|
@ -32,6 +34,7 @@ export const CopyToClipboard: React.FunctionComponent<{
|
|||
}, () => {
|
||||
setIcon('close');
|
||||
});
|
||||
});
|
||||
|
||||
}, [value]);
|
||||
return <ToolbarButton title={description ? description : 'Copy'} icon={icon} onClick={handleCopy}/>;
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ function monotonicTimeDeltaBetweenLibraryAndRunner(nonPrimaryContexts: ContextEn
|
|||
for (const action of context.actions) {
|
||||
if (!action.startTime)
|
||||
continue;
|
||||
const key = matchByStepId ? action.stepId! : `${action.apiName}@${(action as any).wallTime}`;
|
||||
const key = matchByStepId ? action.callId! : `${action.apiName}@${(action as any).wallTime}`;
|
||||
const libraryAction = libraryActions.get(key);
|
||||
if (libraryAction)
|
||||
return action.startTime - libraryAction.startTime;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,15 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.network-request-details-copy {
|
||||
display: flex;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.network-request-details-copy button {
|
||||
border-radius: 4px
|
||||
}
|
||||
|
||||
.network-font-preview {
|
||||
font-family: font-preview;
|
||||
font-size: 30px;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import './networkResourceDetails.css';
|
|||
import { TabbedPane } from '@web/components/tabbedPane';
|
||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||
import { generateCurlCommand, generateFetchCall } from '../third_party/devtools';
|
||||
import { CopyToClipboard } from './copyToClipboard';
|
||||
|
||||
export const NetworkResourceDetails: React.FunctionComponent<{
|
||||
resource: ResourceSnapshot;
|
||||
|
|
@ -90,6 +92,13 @@ const RequestTab: React.FunctionComponent<{
|
|||
</> : null}
|
||||
<div className='network-request-details-header'>Request Headers</div>
|
||||
<div className='network-request-details-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
||||
<div className='network-request-details-header'>Copy Request</div>
|
||||
<div className='network-request-details-copy'>
|
||||
As cURL: <CopyToClipboard description='Copy as cURL' value={() => generateCurlCommand(resource)}/>
|
||||
</div>
|
||||
<div className='network-request-details-copy'>
|
||||
As Fetch: <CopyToClipboard description='Copy as Fetch' value={() => generateFetchCall(resource)}/>
|
||||
</div>
|
||||
{requestBody && <div className='network-request-details-header'>Request Body</div>}
|
||||
{requestBody && <CodeMirrorWrapper text={requestBody.text} mimeType={requestBody.mimeType} readOnly lineNumbers={true}/>}
|
||||
</div>;
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export const TraceView: React.FC<{
|
|||
showSourcesFirst={true}
|
||||
fallbackLocation={fallbackLocation}
|
||||
isLive={true}
|
||||
hideTimeline={true}
|
||||
/>;
|
||||
};
|
||||
|
||||
|
|
@ -163,6 +164,7 @@ class Connection {
|
|||
if (method === 'setSources') {
|
||||
const { sources } = params as { sources: Source[] };
|
||||
this._options.setSources(sources);
|
||||
window.playwrightSourcesEchoForTest = sources;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,7 +105,6 @@ export const UIModeView: React.FC<{}> = ({
|
|||
const [singleWorker, setSingleWorker] = React.useState(queryParams.workers === '1');
|
||||
const [showBrowser, setShowBrowser] = React.useState(queryParams.headed);
|
||||
const [updateSnapshots, setUpdateSnapshots] = React.useState(queryParams.updateSnapshots === 'all');
|
||||
const [showRouteActions, setShowRouteActions] = useSetting('show-route-actions', true);
|
||||
const [darkMode, setDarkMode] = useDarkModeSetting();
|
||||
const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false);
|
||||
|
||||
|
|
@ -526,7 +525,6 @@ export const UIModeView: React.FC<{}> = ({
|
|||
</Toolbar>
|
||||
{settingsVisible && <SettingsView settings={[
|
||||
{ value: darkMode, set: setDarkMode, title: 'Dark mode' },
|
||||
{ value: showRouteActions, set: setShowRouteActions, title: 'Show route actions' },
|
||||
{ value: showScreenshot, set: setShowScreenshot, title: 'Show screenshot instead of snapshot' },
|
||||
]} />}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import type { ErrorDescription } from './errorsTab';
|
|||
import type { ConsoleEntry } from './consoleTab';
|
||||
import { ConsoleTab, useConsoleTabModel } from './consoleTab';
|
||||
import type * as modelUtil from './modelUtil';
|
||||
import { isRouteAction } from './modelUtil';
|
||||
import { NetworkTab, useNetworkTabModel } from './networkTab';
|
||||
import { SnapshotTab } from './snapshotTab';
|
||||
import { SourceTab } from './sourceTab';
|
||||
|
|
@ -50,6 +49,7 @@ export const Workbench: React.FunctionComponent<{
|
|||
rootDir?: string,
|
||||
fallbackLocation?: modelUtil.SourceLocation,
|
||||
isLive?: boolean,
|
||||
hideTimeline?: boolean,
|
||||
status?: UITestStatus,
|
||||
annotations?: { type: string; description?: string; }[];
|
||||
inert?: boolean,
|
||||
|
|
@ -57,7 +57,7 @@ export const Workbench: React.FunctionComponent<{
|
|||
onOpenExternally?: (location: modelUtil.SourceLocation) => void,
|
||||
revealSource?: boolean,
|
||||
showSettings?: boolean,
|
||||
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, status, annotations, inert, openPage, onOpenExternally, revealSource, showSettings }) => {
|
||||
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, openPage, onOpenExternally, revealSource, showSettings }) => {
|
||||
const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined);
|
||||
const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined);
|
||||
|
||||
|
|
@ -70,13 +70,8 @@ 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 [showRouteActions, setShowRouteActions] = useSetting('show-route-actions', true);
|
||||
const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false);
|
||||
|
||||
const filteredActions = React.useMemo(() => {
|
||||
return (model?.actions || []).filter(action => showRouteActions || !isRouteAction(action));
|
||||
}, [model, showRouteActions]);
|
||||
|
||||
const setSelectedAction = React.useCallback((action: modelUtil.ActionTraceEventInContext | undefined) => {
|
||||
setSelectedCallId(action?.callId);
|
||||
setRevealedError(undefined);
|
||||
|
|
@ -291,7 +286,7 @@ export const Workbench: React.FunctionComponent<{
|
|||
</div>}
|
||||
<ActionList
|
||||
sdkLanguage={sdkLanguage}
|
||||
actions={filteredActions}
|
||||
actions={model?.actions || []}
|
||||
selectedAction={model ? selectedAction : undefined}
|
||||
selectedTime={selectedTime}
|
||||
setSelectedTime={setSelectedTime}
|
||||
|
|
@ -311,13 +306,12 @@ export const Workbench: React.FunctionComponent<{
|
|||
id: 'settings',
|
||||
title: 'Settings',
|
||||
component: <SettingsView settings={[
|
||||
{ value: showRouteActions, set: setShowRouteActions, title: 'Show route actions' },
|
||||
{ value: showScreenshot, set: setShowScreenshot, title: 'Show screenshot instead of snapshot' }
|
||||
]}/>,
|
||||
};
|
||||
|
||||
return <div className='vbox workbench' {...(inert ? { inert: 'true' } : {})}>
|
||||
<Timeline
|
||||
{!hideTimeline && <Timeline
|
||||
model={model}
|
||||
consoleEntries={consoleModel.entries}
|
||||
boundaries={boundaries}
|
||||
|
|
@ -328,7 +322,7 @@ export const Workbench: React.FunctionComponent<{
|
|||
sdkLanguage={sdkLanguage}
|
||||
selectedTime={selectedTime}
|
||||
setSelectedTime={setSelectedTime}
|
||||
/>
|
||||
/>}
|
||||
<SplitView
|
||||
sidebarSize={250}
|
||||
orientation={sidebarLocation === 'bottom' ? 'vertical' : 'horizontal'} settingName='propertiesSidebar'
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
import './toolbarButton.css';
|
||||
import '../third_party/vscode/codicon.css';
|
||||
import * as React from 'react';
|
||||
import { clsx } from '@web/uiUtils';
|
||||
|
||||
export interface ToolbarButtonProps {
|
||||
title: string,
|
||||
|
|
@ -40,11 +41,8 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
|
|||
testId,
|
||||
className,
|
||||
}) => {
|
||||
className = (className || '') + ` toolbar-button ${icon}`;
|
||||
if (toggled)
|
||||
className += ' toggled';
|
||||
return <button
|
||||
className={className}
|
||||
className={clsx(className, 'toolbar-button', icon, toggled && 'toggled')}
|
||||
onMouseDown={preventDefault}
|
||||
onClick={onClick}
|
||||
onDoubleClick={preventDefault}
|
||||
|
|
|
|||
BIN
tests/assets/trace-remote-time-diff.zip
Normal file
BIN
tests/assets/trace-remote-time-diff.zip
Normal file
Binary file not shown.
1949
tests/bidi/expectations/bidi-firefox-nightly-library.txt
Normal file
1949
tests/bidi/expectations/bidi-firefox-nightly-library.txt
Normal file
File diff suppressed because it is too large
Load diff
1968
tests/bidi/expectations/bidi-firefox-nightly-page.txt
Normal file
1968
tests/bidi/expectations/bidi-firefox-nightly-page.txt
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -69,12 +69,16 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
|
|||
await run(false);
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
defaultSameSiteCookieValue: [async ({ browserName, isLinux }, run) => {
|
||||
defaultSameSiteCookieValue: [async ({ browserName, platform }, run) => {
|
||||
if (browserName === 'chromium' || browserName as any === '_bidiChromium')
|
||||
await run('Lax');
|
||||
else if (browserName === 'webkit' && isLinux)
|
||||
else if (browserName === 'webkit' && platform === 'linux')
|
||||
await run('Lax');
|
||||
else if (browserName === 'webkit' && !isLinux)
|
||||
else if (browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) >= 24)
|
||||
// macOS 15 Sequoia onwards
|
||||
await run('Lax');
|
||||
else if (browserName === 'webkit')
|
||||
// Windows + older macOS
|
||||
await run('None');
|
||||
else if (browserName === 'firefox' || browserName as any === '_bidiFirefox')
|
||||
await run('None');
|
||||
|
|
|
|||
|
|
@ -57,14 +57,14 @@ export class TestProxy {
|
|||
this._prependHandler('request', (req: IncomingMessage) => {
|
||||
this.requestUrls.push(req.url);
|
||||
const url = new URL(req.url);
|
||||
url.host = `localhost:${port}`;
|
||||
url.host = `127.0.0.1:${port}`;
|
||||
req.url = url.toString();
|
||||
});
|
||||
this._prependHandler('connect', (req: IncomingMessage) => {
|
||||
if (!options?.allowConnectRequests)
|
||||
return;
|
||||
this.connectHosts.push(req.url);
|
||||
req.url = `localhost:${port}`;
|
||||
req.url = `127.0.0.1:${port}`;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -138,10 +138,10 @@ export async function setupSocksForwardingServer({
|
|||
connections.get(payload.uid)?.destroy();
|
||||
connections.delete(payload.uid);
|
||||
});
|
||||
await socksProxy.listen(port, 'localhost');
|
||||
await socksProxy.listen(port, '127.0.0.1');
|
||||
return {
|
||||
closeProxyServer: () => socksProxy.close(),
|
||||
proxyServerAddr: `socks5://localhost:${port}`,
|
||||
proxyServerAddr: `socks5://127.0.0.1:${port}`,
|
||||
connectHosts,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import * as playwrightLibrary from 'playwright-core';
|
|||
|
||||
export type TestModeWorkerOptions = {
|
||||
mode: TestModeName;
|
||||
codegenMode: 'trace-events' | 'actions';
|
||||
};
|
||||
|
||||
export type TestModeTestFixtures = {
|
||||
|
|
@ -48,6 +49,7 @@ export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOpti
|
|||
await run(playwright);
|
||||
await testMode.teardown();
|
||||
}, { scope: 'worker' }],
|
||||
codegenMode: ['actions', { scope: 'worker', option: true }],
|
||||
|
||||
toImplInWorkerScope: [async ({ playwright }, use) => {
|
||||
await use((playwright as any)._toImpl);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import type net from 'net';
|
|||
import path from 'path';
|
||||
import url from 'url';
|
||||
import util from 'util';
|
||||
import type stream from 'stream';
|
||||
import ws from 'ws';
|
||||
import zlib, { gzip } from 'zlib';
|
||||
import { createHttpServer, createHttpsServer } from '../../../packages/playwright-core/lib/utils/network';
|
||||
|
|
@ -31,6 +32,11 @@ const rejectSymbol = Symbol('reject callback');
|
|||
|
||||
const gzipAsync = util.promisify(gzip.bind(zlib));
|
||||
|
||||
type UpgradeActions = {
|
||||
doUpgrade: () => void;
|
||||
socket: stream.Duplex;
|
||||
};
|
||||
|
||||
export class TestServer {
|
||||
private _server: http.Server;
|
||||
private _wsServer: ws.WebSocketServer;
|
||||
|
|
@ -44,6 +50,7 @@ export class TestServer {
|
|||
private _extraHeaders = new Map<string, object>();
|
||||
private _gzipRoutes = new Set<string>();
|
||||
private _requestSubscribers = new Map<string, Promise<any>>();
|
||||
private _upgradeCallback: (actions: UpgradeActions) => void | undefined;
|
||||
readonly PORT: number;
|
||||
readonly PREFIX: string;
|
||||
readonly CROSS_PROCESS_PREFIX: string;
|
||||
|
|
@ -73,6 +80,16 @@ export class TestServer {
|
|||
this._server.on('connection', socket => this._onSocket(socket));
|
||||
this._wsServer = new ws.WebSocketServer({ noServer: true });
|
||||
this._server.on('upgrade', async (request, socket, head) => {
|
||||
const doUpgrade = () => {
|
||||
this._wsServer.handleUpgrade(request, socket, head, ws => {
|
||||
// Next emit is only for our internal 'connection' listeners.
|
||||
this._wsServer.emit('connection', ws, request);
|
||||
});
|
||||
};
|
||||
if (this._upgradeCallback) {
|
||||
this._upgradeCallback({ doUpgrade, socket });
|
||||
return;
|
||||
}
|
||||
const pathname = url.parse(request.url!).path;
|
||||
if (pathname === '/ws-401') {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\nUnauthorized body');
|
||||
|
|
@ -86,10 +103,7 @@ export class TestServer {
|
|||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
this._wsServer.handleUpgrade(request, socket, head, ws => {
|
||||
// Next emit is only for our internal 'connection' listeners.
|
||||
this._wsServer.emit('connection', ws, request);
|
||||
});
|
||||
doUpgrade();
|
||||
});
|
||||
this._server.listen(port);
|
||||
this._dirPath = dirPath;
|
||||
|
|
@ -177,6 +191,8 @@ export class TestServer {
|
|||
this._csp.clear();
|
||||
this._extraHeaders.clear();
|
||||
this._gzipRoutes.clear();
|
||||
this._upgradeCallback = undefined;
|
||||
this._wsServer.removeAllListeners('connection');
|
||||
this._server.closeAllConnections();
|
||||
const error = new Error('Static Server has been reset');
|
||||
for (const subscriber of this._requestSubscribers.values())
|
||||
|
|
@ -294,6 +310,14 @@ export class TestServer {
|
|||
});
|
||||
}
|
||||
|
||||
waitForUpgrade() {
|
||||
return new Promise<UpgradeActions>(fulfill => this._upgradeCallback = fulfill);
|
||||
}
|
||||
|
||||
waitForWebSocket() {
|
||||
return new Promise<ws.WebSocket>(fulfill => this._wsServer.once('connection', (ws, req) => fulfill(ws)));
|
||||
}
|
||||
|
||||
sendOnWebSocketConnection(data) {
|
||||
this.onceWebSocketConnection(ws => ws.send(data));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,10 +71,12 @@ class TraceViewerPage {
|
|||
return await this.page.waitForSelector(`.list-view-entry:has-text("${action}") .action-icons`);
|
||||
}
|
||||
|
||||
@step
|
||||
async selectAction(title: string, ordinal: number = 0) {
|
||||
await this.page.locator(`.action-title:has-text("${title}")`).nth(ordinal).click();
|
||||
}
|
||||
|
||||
@step
|
||||
async selectSnapshot(name: string) {
|
||||
await this.page.click(`.snapshot-tab .tabbed-pane-tab-label:has-text("${name}")`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
|
|||
export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: (EventTraceEvent | ConsoleMessageTraceEvent)[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[], errors: string[] }> {
|
||||
const backend = new TraceBackend(file);
|
||||
const traceModel = new TraceModel();
|
||||
await traceModel.load(backend, () => {});
|
||||
await traceModel.load(backend, false, () => {});
|
||||
const model = new MultiTraceModel(traceModel.contextEntries);
|
||||
const { rootItem } = buildActionTree(model.actions);
|
||||
const actionTree: string[] = [];
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import os from 'os';
|
||||
import { contextTest as it, expect } from '../config/browserTest';
|
||||
|
||||
it('should return no cookies in pristine browser context', async ({ context, page, server }) => {
|
||||
|
|
@ -396,7 +397,7 @@ it('should support requestStorageAccess', async ({ page, server, channel, browse
|
|||
server.waitForRequest('/title.html'),
|
||||
frame.evaluate(() => fetch('/title.html'))
|
||||
]);
|
||||
if (isLinux && browserName === 'webkit')
|
||||
if ((isLinux || (isMac && parseInt(os.release(), 10) >= 24)) && browserName === 'webkit')
|
||||
expect(serverRequest.headers.cookie).toBe(undefined);
|
||||
else
|
||||
expect(serverRequest.headers.cookie).toBe('name=value');
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { attachFrame } from 'tests/config/utils';
|
||||
import { browserTest as it, expect } from '../config/browserTest';
|
||||
import fs from 'fs';
|
||||
|
||||
|
|
@ -275,3 +276,43 @@ it('should work when service worker is intefering', async ({ page, context, serv
|
|||
const storageState = await context.storageState();
|
||||
expect(storageState.origins[0].localStorage[0]).toEqual({ name: 'foo', value: 'bar' });
|
||||
});
|
||||
|
||||
it('should set local storage in third-party context', async ({ contextFactory, server }) => {
|
||||
const context = await contextFactory({
|
||||
storageState: {
|
||||
cookies: [],
|
||||
origins: [
|
||||
{
|
||||
origin: server.CROSS_PROCESS_PREFIX,
|
||||
localStorage: [{
|
||||
name: 'name1',
|
||||
value: 'value1'
|
||||
}]
|
||||
},
|
||||
]
|
||||
}
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const frame = await attachFrame(page, 'frame1', server.CROSS_PROCESS_PREFIX + '/empty.html');
|
||||
|
||||
const localStorage = await frame.evaluate('window.localStorage');
|
||||
expect(localStorage).toEqual({ name1: 'value1' });
|
||||
await context.close();
|
||||
});
|
||||
|
||||
it('should roundtrip local storage in third-party context', async ({ page, contextFactory, server }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const frame = await attachFrame(page, 'frame1', server.CROSS_PROCESS_PREFIX + '/empty.html');
|
||||
await frame.evaluate(() => window.localStorage.setItem('name1', 'value1'));
|
||||
const storageState = await page.context().storageState();
|
||||
|
||||
const context2 = await contextFactory({ storageState });
|
||||
const page2 = await context2.newPage();
|
||||
await page2.goto(server.EMPTY_PAGE);
|
||||
const frame2 = await attachFrame(page2, 'frame1', server.CROSS_PROCESS_PREFIX + '/empty.html');
|
||||
|
||||
const localStorage = await frame2.evaluate('window.localStorage');
|
||||
expect(localStorage).toEqual({ name1: 'value1' });
|
||||
await context2.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ const { createHttpsServer, createHttp2Server } = require('../../packages/playwri
|
|||
|
||||
type TestOptions = {
|
||||
startCCServer(options?: {
|
||||
host?: string;
|
||||
http2?: boolean;
|
||||
enableHTTP1FallbackWhenUsingHttp2?: boolean;
|
||||
useFakeLocalhost?: boolean;
|
||||
|
|
@ -68,8 +67,8 @@ const test = base.extend<TestOptions>({
|
|||
}
|
||||
res.end(parts.map(({ key, value }) => `<div data-testid="${key}">${value}</div>`).join(''));
|
||||
});
|
||||
await new Promise<void>(f => server.listen(0, options?.host ?? 'localhost', () => f()));
|
||||
const host = options?.useFakeLocalhost ? 'local.playwright' : 'localhost';
|
||||
await new Promise<void>(f => server.listen(0, '127.0.0.1', () => f()));
|
||||
const host = options?.useFakeLocalhost ? 'local.playwright' : '127.0.0.1';
|
||||
return `https://${host}:${(server.address() as net.AddressInfo).port}/`;
|
||||
});
|
||||
if (server)
|
||||
|
|
@ -195,7 +194,7 @@ test.describe('fetch', () => {
|
|||
});
|
||||
|
||||
test('pass with trusted client certificates and when a socks proxy is used', async ({ playwright, startCCServer, asset }) => {
|
||||
const serverURL = await startCCServer({ host: '127.0.0.1' });
|
||||
const serverURL = await startCCServer();
|
||||
const serverPort = parseInt(new URL(serverURL).port, 10);
|
||||
const { proxyServerAddr, closeProxyServer, connectHosts } = await setupSocksForwardingServer({
|
||||
port: test.info().workerIndex + 2048 + 2,
|
||||
|
|
@ -366,13 +365,14 @@ test.describe('browser', () => {
|
|||
});
|
||||
expect(proxyServer.connectHosts).toEqual([]);
|
||||
await page.goto(serverURL);
|
||||
expect([...new Set(proxyServer.connectHosts)]).toEqual([`localhost:${new URL(serverURL).port}`]);
|
||||
const host = browserName === 'webkit' && process.platform === 'darwin' ? 'localhost' : '127.0.0.1';
|
||||
expect([...new Set(proxyServer.connectHosts)]).toEqual([`${host}:${new URL(serverURL).port}`]);
|
||||
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('should pass with matching certificates and when a socks proxy is used', async ({ browser, startCCServer, asset, browserName }) => {
|
||||
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin', host: '127.0.0.1' });
|
||||
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||
const serverPort = parseInt(new URL(serverURL).port, 10);
|
||||
const { proxyServerAddr, closeProxyServer, connectHosts } = await setupSocksForwardingServer({
|
||||
port: test.info().workerIndex + 2048 + 2,
|
||||
|
|
@ -390,7 +390,8 @@ test.describe('browser', () => {
|
|||
});
|
||||
expect(connectHosts).toEqual([]);
|
||||
await page.goto(serverURL);
|
||||
expect(connectHosts).toEqual([`localhost:${serverPort}`]);
|
||||
const host = browserName === 'webkit' && process.platform === 'darwin' ? 'localhost' : '127.0.0.1';
|
||||
expect(connectHosts).toEqual([`${host}:${serverPort}`]);
|
||||
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
|
||||
await page.close();
|
||||
await closeProxyServer();
|
||||
|
|
@ -625,7 +626,7 @@ test.describe('browser', () => {
|
|||
});
|
||||
|
||||
test('should pass with matching certificates on context APIRequestContext instance', async ({ browser, startCCServer, asset, browserName }) => {
|
||||
const serverURL = await startCCServer({ host: '127.0.0.1' });
|
||||
const serverURL = await startCCServer();
|
||||
const baseOptions = {
|
||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
|
|
@ -688,7 +689,7 @@ test.describe('browser', () => {
|
|||
}],
|
||||
});
|
||||
{
|
||||
await page.goto(serverURL.replace('localhost', 'local.playwright'));
|
||||
await page.goto(serverURL.replace('127.0.0.1', 'local.playwright'));
|
||||
await expect(page.getByTestId('message')).toHaveText('Sorry, but you need to provide a client certificate to continue.');
|
||||
await expect(page.getByTestId('alpn-protocol')).toHaveText('h2');
|
||||
await expect(page.getByTestId('servername')).toHaveText('local.playwright');
|
||||
|
|
@ -714,7 +715,7 @@ test.describe('browser', () => {
|
|||
}],
|
||||
});
|
||||
{
|
||||
await page.goto(serverURL.replace('localhost', 'local.playwright'));
|
||||
await page.goto(serverURL.replace('127.0.0.1', 'local.playwright'));
|
||||
await expect(page.getByTestId('message')).toHaveText('Sorry, but you need to provide a client certificate to continue.');
|
||||
await expect(page.getByTestId('alpn-protocol')).toHaveText('http/1.1');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -610,6 +610,7 @@ it('should have security details', async ({ contextFactory, httpsServer, browser
|
|||
|
||||
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
|
||||
await page.goto(httpsServer.EMPTY_PAGE);
|
||||
await page.request.get(httpsServer.EMPTY_PAGE);
|
||||
const log = await getLog();
|
||||
const { serverIPAddress, _serverPort: port, _securityDetails: securityDetails } = log.entries[0];
|
||||
if (!mode.startsWith('service')) {
|
||||
|
|
@ -620,6 +621,8 @@ it('should have security details', async ({ contextFactory, httpsServer, browser
|
|||
expect(securityDetails).toEqual({ protocol: 'TLS 1.3', subjectName: 'playwright-test', validFrom: 1691708270, validTo: 2007068270 });
|
||||
else
|
||||
expect(securityDetails).toEqual({ issuer: 'playwright-test', protocol: 'TLS 1.3', subjectName: 'playwright-test', validFrom: 1691708270, validTo: 2007068270 });
|
||||
|
||||
expect(log.entries[1]._securityDetails).toEqual({ issuer: 'playwright-test', protocol: 'TLSv1.3', subjectName: 'playwright-test', validFrom: 1691708270, validTo: 2007068270 });
|
||||
});
|
||||
|
||||
it('should have connection details for redirects', async ({ contextFactory, server, browserName, mode }, testInfo) => {
|
||||
|
|
@ -820,6 +823,7 @@ it('should include API request', async ({ contextFactory, server }, testInfo) =>
|
|||
expect(entry.response.headers.find(h => h.name.toLowerCase() === 'content-type')?.value).toContain('application/json');
|
||||
expect(entry.response.content.size).toBe(15);
|
||||
expect(entry.response.content.text).toBe(responseBody.toString());
|
||||
expect(entry.response.bodySize).toBe(15);
|
||||
|
||||
expect(entry.time).toBeGreaterThan(0);
|
||||
expect(entry.timings).toEqual(expect.objectContaining({
|
||||
|
|
@ -831,6 +835,9 @@ it('should include API request', async ({ contextFactory, server }, testInfo) =>
|
|||
ssl: expect.any(Number),
|
||||
wait: expect.any(Number),
|
||||
}));
|
||||
|
||||
expect(entry.serverIPAddress).toBeDefined();
|
||||
expect(entry._serverPort).toEqual(server.PORT);
|
||||
});
|
||||
|
||||
it('should respect minimal mode for API Requests', async ({ contextFactory, server }, testInfo) => {
|
||||
|
|
@ -844,6 +851,11 @@ it('should respect minimal mode for API Requests', async ({ contextFactory, serv
|
|||
expect(entries).toHaveLength(1);
|
||||
const [entry] = entries;
|
||||
expect(entry.timings).toEqual({ receive: -1, send: -1, wait: -1 });
|
||||
expect(entry.serverIPAddress).toBeUndefined();
|
||||
expect(entry._serverPort).toBeUndefined();
|
||||
expect(entry.request.cookies).toEqual([]);
|
||||
expect(entry.request.bodySize).toBe(-1);
|
||||
expect(entry.response.bodySize).toBe(-1);
|
||||
});
|
||||
|
||||
it('should include redirects from API request', async ({ contextFactory, server }, testInfo) => {
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import type { ConsoleMessage } from 'playwright';
|
|||
test.describe('cli codegen', () => {
|
||||
test.skip(({ mode }) => mode !== 'default');
|
||||
|
||||
test('should click', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should click', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<button onclick="console.log('click')">Submit</button>`);
|
||||
|
||||
|
|
@ -52,8 +52,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`)
|
|||
expect(message.text()).toBe('click');
|
||||
});
|
||||
|
||||
test('should double click', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should double click', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<button onclick="console.log('click ' + event.detail)" ondblclick="console.log('dblclick ' + event.detail)">Submit</button>`);
|
||||
|
||||
|
|
@ -93,8 +93,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).DblClickAsync()
|
|||
]);
|
||||
});
|
||||
|
||||
test('should ignore programmatic events', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should ignore programmatic events', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<button onclick="console.log('click')">Submit</button>`);
|
||||
|
||||
|
|
@ -113,8 +113,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).DblClickAsync()
|
|||
expect(clicks.length).toBe(1);
|
||||
});
|
||||
|
||||
test('should click after same-document navigation', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should click after same-document navigation', async ({ openRecorder, server }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
server.setRoute('/foo.html', (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
|
|
@ -143,8 +143,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).DblClickAsync()
|
|||
expect(message.text()).toBe('click');
|
||||
});
|
||||
|
||||
test('should make a positioned click on a canvas', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should make a positioned click on a canvas', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<canvas width="500" height="500" style="margin: 42px"/>
|
||||
|
|
@ -196,8 +196,8 @@ await page.Locator("canvas").ClickAsync(new LocatorClickOptions
|
|||
expect(message.text()).toBe('click 250 250');
|
||||
});
|
||||
|
||||
test('should work with TrustedTypes', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should work with TrustedTypes', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<head>
|
||||
|
|
@ -234,8 +234,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`)
|
|||
expect(message.text()).toBe('click');
|
||||
});
|
||||
|
||||
test('should not target selector preview by text regexp', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should not target selector preview by text regexp', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<span>dummy</span>`);
|
||||
|
||||
|
|
@ -265,8 +265,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`)
|
|||
expect(message.text()).toBe('click');
|
||||
});
|
||||
|
||||
test('should fill', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should fill', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input id="input" name="name" oninput="console.log(input.value)"></input>`);
|
||||
const locator = await recorder.focusElement('input');
|
||||
|
|
@ -295,8 +295,8 @@ await page.Locator("#input").FillAsync(\"John\");`);
|
|||
expect(message.text()).toBe('John');
|
||||
});
|
||||
|
||||
test('should fill japanese text', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should fill japanese text', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
// In Japanese, "てすと" or "テスト" means "test".
|
||||
await recorder.setContentAndWait(`<input id="input" name="name" oninput="input.value === 'てすと' && console.log(input.value)"></input>`);
|
||||
|
|
@ -329,8 +329,8 @@ await page.Locator("#input").FillAsync(\"てすと\");`);
|
|||
expect(message.text()).toBe('てすと');
|
||||
});
|
||||
|
||||
test('should fill textarea', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should fill textarea', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<textarea id="textarea" name="name" oninput="console.log(textarea.value)"></textarea>`);
|
||||
const locator = await recorder.focusElement('textarea');
|
||||
|
|
@ -346,9 +346,9 @@ await page.Locator("#input").FillAsync(\"てすと\");`);
|
|||
expect(message.text()).toBe('John');
|
||||
});
|
||||
|
||||
test('should fill textarea with new lines at the end', async ({ page, openRecorder }) => {
|
||||
test('should fill textarea with new lines at the end', async ({ openRecorder }) => {
|
||||
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23774' });
|
||||
const recorder = await openRecorder();
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`<textarea id="textarea"></textarea>`);
|
||||
const textarea = page.locator('textarea');
|
||||
await textarea.evaluate<void, HTMLTextAreaElement>(e => e.addEventListener('input', () => (window as any).lastInputValue = e.value));
|
||||
|
|
@ -361,8 +361,8 @@ await page.Locator("#input").FillAsync(\"てすと\");`);
|
|||
expect(sources.get('JavaScript')!.text).not.toContain(`Enter`);
|
||||
});
|
||||
|
||||
test('should fill [contentEditable]', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should fill [contentEditable]', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<div id="content" contenteditable="" oninput="console.log(content.innerText)"/>`);
|
||||
const locator = await recorder.focusElement('div');
|
||||
|
|
@ -378,8 +378,8 @@ await page.Locator("#input").FillAsync(\"てすと\");`);
|
|||
expect(message.text()).toBe('John Doe');
|
||||
});
|
||||
|
||||
test('should press', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should press', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input name="name" onkeypress="console.log('press')"></input>`);
|
||||
|
||||
|
|
@ -412,8 +412,8 @@ await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`);
|
|||
expect(messages[0].text()).toBe('press');
|
||||
});
|
||||
|
||||
test('should update selected element after pressing Tab', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should update selected element after pressing Tab', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<input name="one"></input>
|
||||
|
|
@ -441,8 +441,8 @@ await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`);
|
|||
await page.locator('input[name="two"]').fill('barfoo321');`);
|
||||
});
|
||||
|
||||
test('should record ArrowDown', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should record ArrowDown', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input name="name" onkeydown="console.log('press:' + event.key)"></input>`);
|
||||
|
||||
|
|
@ -463,8 +463,8 @@ await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`);
|
|||
expect(messages[0].text()).toBe('press:ArrowDown');
|
||||
});
|
||||
|
||||
test('should emit single keyup on ArrowDown', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should emit single keyup on ArrowDown', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input name="name" onkeydown="console.log('down:' + event.key)" onkeyup="console.log('up:' + event.key)"></input>`);
|
||||
|
||||
|
|
@ -488,8 +488,8 @@ await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`);
|
|||
expect(messages[1].text()).toBe('up:ArrowDown');
|
||||
});
|
||||
|
||||
test('should check', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should check', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="console.log(checkbox.checked)"></input>`);
|
||||
|
||||
|
|
@ -520,8 +520,8 @@ await page.Locator("#checkbox").CheckAsync();`);
|
|||
expect(message.text()).toBe('true');
|
||||
});
|
||||
|
||||
test('should check a radio button', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should check a radio button', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input id="checkbox" type="radio" name="accept" onchange="console.log(checkbox.checked)"></input>`);
|
||||
|
||||
|
|
@ -539,8 +539,8 @@ await page.Locator("#checkbox").CheckAsync();`);
|
|||
expect(message.text()).toBe('true');
|
||||
});
|
||||
|
||||
test('should check with keyboard', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should check with keyboard', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="console.log(checkbox.checked)"></input>`);
|
||||
|
||||
|
|
@ -558,8 +558,8 @@ await page.Locator("#checkbox").CheckAsync();`);
|
|||
expect(message.text()).toBe('true');
|
||||
});
|
||||
|
||||
test('should uncheck', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should uncheck', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" checked name="accept" onchange="console.log(checkbox.checked)"></input>`);
|
||||
|
||||
|
|
@ -590,8 +590,8 @@ await page.Locator("#checkbox").UncheckAsync();`);
|
|||
expect(message.text()).toBe('false');
|
||||
});
|
||||
|
||||
test('should select', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should select', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait('<select id="age" onchange="console.log(age.selectedOptions[0].value)"><option value="1"><option value="2"></select>');
|
||||
|
||||
|
|
@ -623,8 +623,8 @@ await page.Locator("#age").SelectOptionAsync(new[] { "2" });`);
|
|||
expect(message.text()).toBe('2');
|
||||
});
|
||||
|
||||
test('should select with size attribute', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should select with size attribute', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<style>
|
||||
|
|
@ -664,8 +664,8 @@ await page.Locator(\"#age\").SelectOptionAsync(new[] { \"2\" });`);
|
|||
expect(message.text()).toBe('2');
|
||||
});
|
||||
|
||||
test('should await popup', async ({ page, openRecorder, browserName, headless }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should await popup', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait('<a target=_blank rel=noopener href="about:blank">link</a>');
|
||||
|
||||
const locator = await recorder.hoverOverElement('a');
|
||||
|
|
@ -706,8 +706,8 @@ var page1 = await page.RunAndWaitForPopupAsync(async () =>
|
|||
expect(popup.url()).toBe('about:blank');
|
||||
});
|
||||
|
||||
test('should attribute navigation to click', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should attribute navigation to click', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<a onclick="window.location.href='about:blank#foo'">link</a>`);
|
||||
|
||||
|
|
@ -763,7 +763,7 @@ await page.GetByText("link").ClickAsync();`);
|
|||
|
||||
test('should ignore AltGraph', async ({ openRecorder, browserName }) => {
|
||||
test.skip(browserName === 'firefox', 'The TextInputProcessor in Firefox does not work with AltGraph.');
|
||||
const recorder = await openRecorder();
|
||||
const { recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`<input></input>`);
|
||||
|
||||
await recorder.page.type('input', 'playwright');
|
||||
|
|
@ -775,8 +775,8 @@ await page.GetByText("link").ClickAsync();`);
|
|||
expect(recorder.sources().get('JavaScript')!.text).toContain(`await page.getByRole('textbox').fill('playwright@example.com');`);
|
||||
});
|
||||
|
||||
test('should middle click', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should middle click', async ({ openRecorder, server }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<a href${JSON.stringify(server.EMPTY_PAGE)}>Click me</a>`);
|
||||
|
||||
|
|
@ -807,8 +807,8 @@ await page.GetByText("Click me").ClickAsync(new LocatorClickOptions
|
|||
});`);
|
||||
});
|
||||
|
||||
test('should record slider', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should record slider', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input type="range" min="0" max="10" value="5">`);
|
||||
|
||||
|
|
@ -846,10 +846,10 @@ await page.GetByText("Click me").ClickAsync(new LocatorClickOptions
|
|||
await page.GetByRole(AriaRole.Slider).FillAsync("10");`);
|
||||
});
|
||||
|
||||
test('should click button with nested div', async ({ page, openRecorder }) => {
|
||||
test('should click button with nested div', async ({ openRecorder }) => {
|
||||
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29067' });
|
||||
|
||||
const recorder = await openRecorder();
|
||||
const { recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<button><div role="none">Submit</div></button>`);
|
||||
|
||||
|
|
@ -878,8 +878,8 @@ await page.GetByRole(AriaRole.Slider).FillAsync("10");`);
|
|||
await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`);
|
||||
});
|
||||
|
||||
test('should record omnibox navigations after performAction', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should record omnibox navigations after performAction', async ({ openRecorder, server }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`<button>Submit</button>`);
|
||||
await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
|
|
@ -890,8 +890,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`)
|
|||
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/empty.html');`);
|
||||
});
|
||||
|
||||
test('should record omnibox navigations after recordAction', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should record omnibox navigations after recordAction', async ({ openRecorder, server }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`<textarea></textarea>`);
|
||||
await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'fill'),
|
||||
|
|
@ -902,8 +902,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`)
|
|||
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/empty.html');`);
|
||||
});
|
||||
|
||||
test('should not throw csp directive violation errors', async ({ page, openRecorder, server }) => {
|
||||
await openRecorder();
|
||||
test('should not throw csp directive violation errors', async ({ openRecorder, server }) => {
|
||||
const { page } = await openRecorder();
|
||||
await page.goto(server.PREFIX + '/csp.html');
|
||||
const predicate = (msg: ConsoleMessage) => msg.type() === 'error' && /Content[\- ]Security[\- ]Policy/i.test(msg.text());
|
||||
await expect(page.waitForEvent('console', { predicate, timeout: 1000 })).rejects.toThrow();
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ test.describe('cli codegen', () => {
|
|||
test.skip(({ mode }) => mode !== 'default');
|
||||
|
||||
test('should contain open page', async ({ openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
const { recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(``);
|
||||
const sources = await recorder.waitForOutput('JavaScript', `page.goto`);
|
||||
|
|
@ -43,8 +43,8 @@ test.describe('cli codegen', () => {
|
|||
var page = await context.NewPageAsync();`);
|
||||
});
|
||||
|
||||
test('should contain second page', async ({ openRecorder, page }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should contain second page', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(``);
|
||||
await page.context().newPage();
|
||||
|
|
@ -66,8 +66,8 @@ var page = await context.NewPageAsync();`);
|
|||
var page1 = await context.NewPageAsync();`);
|
||||
});
|
||||
|
||||
test('should contain close page', async ({ openRecorder, page }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should contain close page', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(``);
|
||||
await page.context().newPage();
|
||||
|
|
@ -90,8 +90,8 @@ var page1 = await context.NewPageAsync();`);
|
|||
await page.CloseAsync();`);
|
||||
});
|
||||
|
||||
test('should not lead to an error if html gets clicked', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should not lead to an error if html gets clicked', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait('');
|
||||
await page.context().newPage();
|
||||
|
|
@ -104,9 +104,9 @@ await page.CloseAsync();`);
|
|||
expect(errors.length).toBe(0);
|
||||
});
|
||||
|
||||
test('should upload a single file', async ({ page, openRecorder, browserName, asset, isLinux }) => {
|
||||
test('should upload a single file', async ({ openRecorder, browserName, asset, isLinux }) => {
|
||||
test.fixme(browserName === 'firefox' && isLinux, 'https://bugzilla.mozilla.org/show_bug.cgi?id=1827551');
|
||||
const recorder = await openRecorder();
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`
|
||||
<form>
|
||||
<input type="file">
|
||||
|
|
@ -135,9 +135,9 @@ await page.CloseAsync();`);
|
|||
await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-upload.txt\" });`);
|
||||
});
|
||||
|
||||
test('should upload multiple files', async ({ page, openRecorder, browserName, asset, isLinux }) => {
|
||||
test('should upload multiple files', async ({ openRecorder, browserName, asset, isLinux }) => {
|
||||
test.fixme(browserName === 'firefox' && isLinux, 'https://bugzilla.mozilla.org/show_bug.cgi?id=1827551');
|
||||
const recorder = await openRecorder();
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`
|
||||
<form>
|
||||
<input type="file" multiple>
|
||||
|
|
@ -166,9 +166,9 @@ await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-uplo
|
|||
await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-upload.txt\", \"file-to-upload-2.txt\" });`);
|
||||
});
|
||||
|
||||
test('should clear files', async ({ page, openRecorder, browserName, asset, isLinux }) => {
|
||||
test('should clear files', async ({ openRecorder, browserName, asset, isLinux }) => {
|
||||
test.fixme(browserName === 'firefox' && isLinux, 'https://bugzilla.mozilla.org/show_bug.cgi?id=1827551');
|
||||
const recorder = await openRecorder();
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`
|
||||
<form>
|
||||
<input type="file" multiple>
|
||||
|
|
@ -197,8 +197,8 @@ await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-uplo
|
|||
await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { });`);
|
||||
});
|
||||
|
||||
test('should download files', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should download files', async ({ openRecorder, server }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
server.setRoute('/download', (req, res) => {
|
||||
const pathName = url.parse(req.url!).path;
|
||||
|
|
@ -273,8 +273,8 @@ var download1 = await page.RunAndWaitForDownloadAsync(async () =>
|
|||
});`);
|
||||
});
|
||||
|
||||
test('should handle dialogs', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should handle dialogs', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<button onclick="alert()">click me</button>
|
||||
|
|
@ -321,8 +321,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "click me" }).ClickAsync();
|
|||
|
||||
});
|
||||
|
||||
test('should handle history.postData', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should handle history.postData', async ({ openRecorder, server }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<script>
|
||||
|
|
@ -337,8 +337,9 @@ await page.GetByRole(AriaRole.Button, new() { Name = "click me" }).ClickAsync();
|
|||
}
|
||||
});
|
||||
|
||||
test('should record open in a new tab with url', async ({ page, openRecorder, browserName }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should record open in a new tab with url', async ({ openRecorder, browserName, codegenMode }) => {
|
||||
test.skip(codegenMode === 'trace-events');
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`<a href="about:blank?foo">link</a>`);
|
||||
|
||||
const locator = await recorder.hoverOverElement('a');
|
||||
|
|
@ -367,8 +368,8 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
}
|
||||
});
|
||||
|
||||
test('should not clash pages', async ({ page, openRecorder, browserName }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should not clash pages', async ({ openRecorder, browserName }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
const [popup1] = await Promise.all([
|
||||
page.context().waitForEvent('page'),
|
||||
page.evaluate(`window.open('about:blank')`)
|
||||
|
|
@ -404,8 +405,8 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
expect(sources.get('C#')!.text).toContain(`await page2.Locator("#name").FillAsync("TextB");`);
|
||||
});
|
||||
|
||||
test('click should emit events in order', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('click should emit events in order', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<button id=button>
|
||||
|
|
@ -428,8 +429,8 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
expect(messages).toEqual(['mousedown', 'mouseup', 'click']);
|
||||
});
|
||||
|
||||
test('should update hover model on action', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should update hover model on action', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name='updated'"></input>`);
|
||||
const [models] = await Promise.all([
|
||||
|
|
@ -439,8 +440,8 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
expect(models.hovered).toBe('#checkbox');
|
||||
});
|
||||
|
||||
test('should reset hover model on action when element detaches', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should reset hover model on action when element detaches', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input id="checkbox" onclick="document.getElementById('checkbox').remove()">`);
|
||||
const [models] = await Promise.all([
|
||||
|
|
@ -450,10 +451,10 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
expect(models.hovered).toBe(null);
|
||||
});
|
||||
|
||||
test('should update active model on action', async ({ page, openRecorder, browserName, headless }) => {
|
||||
test('should update active model on action', async ({ openRecorder, browserName, headless }) => {
|
||||
test.fixme(browserName === 'webkit');
|
||||
|
||||
const recorder = await openRecorder();
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name='updated'"></input>`);
|
||||
const [models] = await Promise.all([
|
||||
recorder.waitForActionPerformed(),
|
||||
|
|
@ -462,8 +463,8 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
expect(models.active).toBe('#checkbox');
|
||||
});
|
||||
|
||||
test('should check input with chaining id', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should check input with chaining id', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name = 'updated'"></input>`);
|
||||
await Promise.all([
|
||||
recorder.waitForActionPerformed(),
|
||||
|
|
@ -471,8 +472,8 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
]);
|
||||
});
|
||||
|
||||
test('should record navigations after identical pushState', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should record navigations after identical pushState', async ({ openRecorder, server }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
server.setRoute('/page2.html', (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.end('Hello world');
|
||||
|
|
@ -490,7 +491,8 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`);
|
||||
});
|
||||
|
||||
test('should --save-trace', async ({ runCLI }, testInfo) => {
|
||||
test('should --save-trace', async ({ runCLI, codegenMode }, testInfo) => {
|
||||
test.skip(codegenMode === 'trace-events');
|
||||
const traceFileName = testInfo.outputPath('trace.zip');
|
||||
const cli = runCLI([`--save-trace=${traceFileName}`], {
|
||||
autoExitWhen: ' ',
|
||||
|
|
@ -499,7 +501,8 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
expect(fs.existsSync(traceFileName)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => {
|
||||
test('should save assets via SIGINT', async ({ runCLI, platform, codegenMode }, testInfo) => {
|
||||
test.skip(codegenMode === 'trace-events');
|
||||
test.skip(platform === 'win32', 'SIGINT not supported on Windows');
|
||||
|
||||
const traceFileName = testInfo.outputPath('trace.zip');
|
||||
|
|
@ -515,8 +518,8 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
expect(fs.existsSync(harFileName)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should fill tricky characters', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should fill tricky characters', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<textarea spellcheck=false id="textarea" name="name" oninput="console.log(textarea.value)"></textarea>`);
|
||||
const locator = await recorder.focusElement('textarea');
|
||||
|
|
@ -548,8 +551,8 @@ await page.Locator("#textarea").FillAsync(\"Hello'\\"\`\\nWorld\");`);
|
|||
|
||||
});
|
||||
|
||||
test('should --test-id-attribute', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder({ testIdAttributeName: 'my-test-id' });
|
||||
test('should --test-id-attribute', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder({ testIdAttributeName: 'my-test-id' });
|
||||
|
||||
await recorder.setContentAndWait(`<div my-test-id="foo">Hello</div>`);
|
||||
await page.click('[my-test-id=foo]');
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import { test, expect } from './inspectorTest';
|
|||
test.describe('cli codegen', () => {
|
||||
test.skip(({ mode }) => mode !== 'default');
|
||||
|
||||
test('should click locator.first', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should click locator.first', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<button onclick="console.log('click1')">Submit</button>
|
||||
|
|
@ -54,8 +54,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).First.ClickAsyn
|
|||
expect(message.text()).toBe('click1');
|
||||
});
|
||||
|
||||
test('should click locator.nth', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should click locator.nth', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<button onclick="console.log('click1')">Submit</button>
|
||||
|
|
@ -89,8 +89,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).Nth(1).ClickAsy
|
|||
expect(message.text()).toBe('click2');
|
||||
});
|
||||
|
||||
test('should generate frame locators', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should generate frame locators', async ({ openRecorder, server }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
/*
|
||||
iframe
|
||||
div Hello1
|
||||
|
|
@ -127,13 +127,13 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).Nth(1).ClickAsy
|
|||
page.locator("#frame1").contentFrame().getByText("Hello1").click();`);
|
||||
|
||||
expect.soft(sources.get('Python')!.text).toContain(`
|
||||
page.locator("#frame1").content_frame().get_by_text("Hello1").click()`);
|
||||
page.locator("#frame1").content_frame.get_by_text("Hello1").click()`);
|
||||
|
||||
expect.soft(sources.get('Python Async')!.text).toContain(`
|
||||
await page.locator("#frame1").content_frame().get_by_text("Hello1").click()`);
|
||||
await page.locator("#frame1").content_frame.get_by_text("Hello1").click()`);
|
||||
|
||||
expect.soft(sources.get('C#')!.text).toContain(`
|
||||
await page.Locator("#frame1").ContentFrame().GetByText("Hello1").ClickAsync();`);
|
||||
await page.Locator("#frame1").ContentFrame.GetByText("Hello1").ClickAsync();`);
|
||||
|
||||
|
||||
[sources] = await Promise.all([
|
||||
|
|
@ -148,13 +148,13 @@ await page.Locator("#frame1").ContentFrame().GetByText("Hello1").ClickAsync();`)
|
|||
page.locator("#frame1").contentFrame().locator("iframe").contentFrame().getByText("Hello2").click();`);
|
||||
|
||||
expect.soft(sources.get('Python')!.text).toContain(`
|
||||
page.locator("#frame1").content_frame().locator("iframe").content_frame().get_by_text("Hello2").click()`);
|
||||
page.locator("#frame1").content_frame.locator("iframe").content_frame.get_by_text("Hello2").click()`);
|
||||
|
||||
expect.soft(sources.get('Python Async')!.text).toContain(`
|
||||
await page.locator("#frame1").content_frame().locator("iframe").content_frame().get_by_text("Hello2").click()`);
|
||||
await page.locator("#frame1").content_frame.locator("iframe").content_frame.get_by_text("Hello2").click()`);
|
||||
|
||||
expect.soft(sources.get('C#')!.text).toContain(`
|
||||
await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().GetByText("Hello2").ClickAsync();`);
|
||||
await page.Locator("#frame1").ContentFrame.Locator("iframe").ContentFrame.GetByText("Hello2").ClickAsync();`);
|
||||
|
||||
|
||||
[sources] = await Promise.all([
|
||||
|
|
@ -169,13 +169,13 @@ await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().Ge
|
|||
page.locator("#frame1").contentFrame().locator("iframe").contentFrame().locator("iframe[name=\\"one\\"]").contentFrame().getByText("HelloNameOne").click();`);
|
||||
|
||||
expect.soft(sources.get('Python')!.text).toContain(`
|
||||
page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe[name=\\"one\\"]").content_frame().get_by_text("HelloNameOne").click()`);
|
||||
page.locator("#frame1").content_frame.locator("iframe").content_frame.locator("iframe[name=\\"one\\"]").content_frame.get_by_text("HelloNameOne").click()`);
|
||||
|
||||
expect.soft(sources.get('Python Async')!.text).toContain(`
|
||||
await page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe[name=\\"one\\"]").content_frame().get_by_text("HelloNameOne").click()`);
|
||||
await page.locator("#frame1").content_frame.locator("iframe").content_frame.locator("iframe[name=\\"one\\"]").content_frame.get_by_text("HelloNameOne").click()`);
|
||||
|
||||
expect.soft(sources.get('C#')!.text).toContain(`
|
||||
await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().Locator("iframe[name=\\"one\\"]").ContentFrame().GetByText("HelloNameOne").ClickAsync();`);
|
||||
await page.Locator("#frame1").ContentFrame.Locator("iframe").ContentFrame.Locator("iframe[name=\\"one\\"]").ContentFrame.GetByText("HelloNameOne").ClickAsync();`);
|
||||
|
||||
[sources] = await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'HelloNameAnonymous'),
|
||||
|
|
@ -189,17 +189,17 @@ await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().Lo
|
|||
page.locator("#frame1").contentFrame().locator("iframe").contentFrame().locator("iframe").nth(2).contentFrame().getByText("HelloNameAnonymous").click();`);
|
||||
|
||||
expect.soft(sources.get('Python')!.text).toContain(`
|
||||
page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe").nth(2).content_frame().get_by_text("HelloNameAnonymous").click()`);
|
||||
page.locator("#frame1").content_frame.locator("iframe").content_frame.locator("iframe").nth(2).content_frame.get_by_text("HelloNameAnonymous").click()`);
|
||||
|
||||
expect.soft(sources.get('Python Async')!.text).toContain(`
|
||||
await page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe").nth(2).content_frame().get_by_text("HelloNameAnonymous").click()`);
|
||||
await page.locator("#frame1").content_frame.locator("iframe").content_frame.locator("iframe").nth(2).content_frame.get_by_text("HelloNameAnonymous").click()`);
|
||||
|
||||
expect.soft(sources.get('C#')!.text).toContain(`
|
||||
await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().Locator("iframe").Nth(2).ContentFrame().GetByText("HelloNameAnonymous").ClickAsync();`);
|
||||
await page.Locator("#frame1").ContentFrame.Locator("iframe").ContentFrame.Locator("iframe").Nth(2).ContentFrame.GetByText("HelloNameAnonymous").ClickAsync();`);
|
||||
});
|
||||
|
||||
test('should generate frame locators with special characters in name attribute', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should generate frame locators with special characters in name attribute', async ({ openRecorder, server }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`
|
||||
<iframe srcdoc="<button>Click me</button>">
|
||||
`, server.EMPTY_PAGE, 2);
|
||||
|
|
@ -217,17 +217,17 @@ await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().Lo
|
|||
page.locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Click me")).click()`);
|
||||
|
||||
expect.soft(sources.get('Python')!.text).toContain(`
|
||||
page.locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").content_frame().get_by_role("button", name="Click me").click()`);
|
||||
page.locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").content_frame.get_by_role("button", name="Click me").click()`);
|
||||
|
||||
expect.soft(sources.get('Python Async')!.text).toContain(`
|
||||
await page.locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").content_frame().get_by_role("button", name="Click me").click()`);
|
||||
await page.locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").content_frame.get_by_role("button", name="Click me").click()`);
|
||||
|
||||
expect.soft(sources.get('C#')!.text).toContain(`
|
||||
await page.Locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`);
|
||||
await page.Locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").ContentFrame.GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`);
|
||||
});
|
||||
|
||||
test('should generate frame locators with title attribute', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should generate frame locators with title attribute', async ({ openRecorder, server }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`
|
||||
<iframe title="hello world" srcdoc="<button>Click me</button>"></iframe>
|
||||
`, server.EMPTY_PAGE, 1);
|
||||
|
|
@ -246,20 +246,20 @@ await page.Locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").Cont
|
|||
);
|
||||
|
||||
expect.soft(sources.get('Python')!.text).toContain(
|
||||
`page.locator(\"iframe[title=\\\"hello world\\\"]\").content_frame().get_by_role(\"button\", name=\"Click me\").click()`
|
||||
`page.locator(\"iframe[title=\\\"hello world\\\"]\").content_frame.get_by_role(\"button\", name=\"Click me\").click()`
|
||||
);
|
||||
|
||||
expect.soft(sources.get('Python Async')!.text).toContain(
|
||||
`await page.locator("iframe[title=\\\"hello world\\\"]").content_frame().get_by_role("button", name="Click me").click()`
|
||||
`await page.locator("iframe[title=\\\"hello world\\\"]").content_frame.get_by_role("button", name="Click me").click()`
|
||||
);
|
||||
|
||||
expect.soft(sources.get('C#')!.text).toContain(
|
||||
`await page.Locator("iframe[title=\\\"hello world\\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
|
||||
`await page.Locator("iframe[title=\\\"hello world\\\"]").ContentFrame.GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
|
||||
);
|
||||
});
|
||||
|
||||
test('should generate frame locators with name attribute', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should generate frame locators with name attribute', async ({ openRecorder, server }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`
|
||||
<iframe name="hello world" srcdoc="<button>Click me</button>"></iframe>
|
||||
`, server.EMPTY_PAGE, 1);
|
||||
|
|
@ -278,20 +278,20 @@ await page.Locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").Cont
|
|||
);
|
||||
|
||||
expect.soft(sources.get('Python')!.text).toContain(
|
||||
`page.locator(\"iframe[name=\\\"hello world\\\"]\").content_frame().get_by_role(\"button\", name=\"Click me\").click()`
|
||||
`page.locator(\"iframe[name=\\\"hello world\\\"]\").content_frame.get_by_role(\"button\", name=\"Click me\").click()`
|
||||
);
|
||||
|
||||
expect.soft(sources.get('Python Async')!.text).toContain(
|
||||
`await page.locator("iframe[name=\\\"hello world\\\"]").content_frame().get_by_role("button", name="Click me").click()`
|
||||
`await page.locator("iframe[name=\\\"hello world\\\"]").content_frame.get_by_role("button", name="Click me").click()`
|
||||
);
|
||||
|
||||
expect.soft(sources.get('C#')!.text).toContain(
|
||||
`await page.Locator("iframe[name=\\\"hello world\\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
|
||||
`await page.Locator("iframe[name=\\\"hello world\\\"]").ContentFrame.GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
|
||||
);
|
||||
});
|
||||
|
||||
test('should generate frame locators with id attribute', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should generate frame locators with id attribute', async ({ openRecorder, server }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`
|
||||
<iframe id="hello-world" srcdoc="<button>Click me</button>"></iframe>
|
||||
`, server.EMPTY_PAGE, 1);
|
||||
|
|
@ -310,20 +310,20 @@ await page.Locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").Cont
|
|||
);
|
||||
|
||||
expect.soft(sources.get('Python')!.text).toContain(
|
||||
`page.locator(\"#hello-world\").content_frame().get_by_role(\"button\", name=\"Click me\").click()`
|
||||
`page.locator(\"#hello-world\").content_frame.get_by_role(\"button\", name=\"Click me\").click()`
|
||||
);
|
||||
|
||||
expect.soft(sources.get('Python Async')!.text).toContain(
|
||||
`await page.locator("#hello-world").content_frame().get_by_role("button", name="Click me").click()`
|
||||
`await page.locator("#hello-world").content_frame.get_by_role("button", name="Click me").click()`
|
||||
);
|
||||
|
||||
expect.soft(sources.get('C#')!.text).toContain(
|
||||
`await page.Locator("#hello-world").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
|
||||
`await page.Locator("#hello-world").ContentFrame.GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
|
||||
);
|
||||
});
|
||||
|
||||
test('should generate frame locators with testId', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should generate frame locators with testId', async ({ openRecorder, server }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`
|
||||
<iframe data-testid="my-testid" srcdoc="<button>Click me</button>"></iframe>
|
||||
`, server.EMPTY_PAGE, 1);
|
||||
|
|
@ -342,20 +342,20 @@ await page.Locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").Cont
|
|||
);
|
||||
|
||||
expect.soft(sources.get('Python')!.text).toContain(
|
||||
`page.locator(\"[data-testid=\\\"my-testid\\\"]\").content_frame().get_by_role(\"button\", name=\"Click me\").click()`
|
||||
`page.locator(\"[data-testid=\\\"my-testid\\\"]\").content_frame.get_by_role(\"button\", name=\"Click me\").click()`
|
||||
);
|
||||
|
||||
expect.soft(sources.get('Python Async')!.text).toContain(
|
||||
`await page.locator("[data-testid=\\\"my-testid\\\"]").content_frame().get_by_role("button", name="Click me").click()`
|
||||
`await page.locator("[data-testid=\\\"my-testid\\\"]").content_frame.get_by_role("button", name="Click me").click()`
|
||||
);
|
||||
|
||||
expect.soft(sources.get('C#')!.text).toContain(
|
||||
`await page.Locator("[data-testid=\\\"my-testid\\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
|
||||
`await page.Locator("[data-testid=\\\"my-testid\\\"]").ContentFrame.GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
|
||||
);
|
||||
});
|
||||
|
||||
test('should generate role locators undef frame locators', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should generate role locators undef frame locators', async ({ openRecorder, server }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`<iframe id=frame1 srcdoc="<button>Submit</button>">`, server.EMPTY_PAGE, 2);
|
||||
const frame = page.mainFrame().childFrames()[0];
|
||||
|
||||
|
|
@ -371,17 +371,17 @@ await page.Locator("iframe[name=\\"foo\\\\<bar\\\\'\\\\\\"\\\\\`\\\\>\\"]").Cont
|
|||
page.locator("#frame1").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Submit")).click();`);
|
||||
|
||||
expect.soft(sources.get('Python')!.text).toContain(`
|
||||
page.locator("#frame1").content_frame().get_by_role("button", name="Submit").click()`);
|
||||
page.locator("#frame1").content_frame.get_by_role("button", name="Submit").click()`);
|
||||
|
||||
expect.soft(sources.get('Python Async')!.text).toContain(`
|
||||
await page.locator("#frame1").content_frame().get_by_role("button", name="Submit").click()`);
|
||||
await page.locator("#frame1").content_frame.get_by_role("button", name="Submit").click()`);
|
||||
|
||||
expect.soft(sources.get('C#')!.text).toContain(`
|
||||
await page.Locator("#frame1").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`);
|
||||
await page.Locator("#frame1").ContentFrame.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`);
|
||||
});
|
||||
|
||||
test('should generate getByTestId', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should generate getByTestId', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<div data-testid=testid onclick="console.log('click')">Submit</div>`);
|
||||
|
||||
|
|
@ -412,8 +412,8 @@ await page.GetByTestId("testid").ClickAsync();`);
|
|||
expect(message.text()).toBe('click');
|
||||
});
|
||||
|
||||
test('should generate getByPlaceholder', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should generate getByPlaceholder', async ({ openRecorder }) => {
|
||||
const { recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input placeholder="Country"></input>`);
|
||||
|
||||
|
|
@ -441,8 +441,8 @@ await page.GetByTestId("testid").ClickAsync();`);
|
|||
await page.GetByPlaceholder("Country").ClickAsync();`);
|
||||
});
|
||||
|
||||
test('should generate getByAltText', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should generate getByAltText', async ({ openRecorder }) => {
|
||||
const { recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input alt="Country"></input>`);
|
||||
|
||||
|
|
@ -470,8 +470,8 @@ await page.GetByPlaceholder("Country").ClickAsync();`);
|
|||
await page.GetByAltText("Country").ClickAsync();`);
|
||||
});
|
||||
|
||||
test('should generate getByLabel', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should generate getByLabel', async ({ openRecorder }) => {
|
||||
const { recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<label for=target>Country</label><input id=target>`);
|
||||
|
||||
|
|
@ -499,8 +499,8 @@ await page.GetByAltText("Country").ClickAsync();`);
|
|||
await page.GetByLabel("Country").ClickAsync();`);
|
||||
});
|
||||
|
||||
test('should generate getByLabel without regex', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should generate getByLabel without regex', async ({ openRecorder }) => {
|
||||
const { recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<label for=target>Coun"try</label><input id=target>`);
|
||||
|
||||
|
|
@ -528,8 +528,8 @@ await page.GetByLabel("Country").ClickAsync();`);
|
|||
await page.GetByLabel("Coun\\"try").ClickAsync();`);
|
||||
});
|
||||
|
||||
test('should consume pointer events', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should consume pointer events', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<button onclick="console.log('clicked')">Submit</button>
|
||||
|
|
@ -559,8 +559,8 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
|
|||
]);
|
||||
});
|
||||
|
||||
test('should consume contextmenu events, despite a custom context menu', async ({ page, openRecorder, browserName, platform }) => {
|
||||
const recorder = await openRecorder();
|
||||
test('should consume contextmenu events, despite a custom context menu', async ({ openRecorder, browserName, platform }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<button>Right click me.</button>
|
||||
|
|
@ -630,7 +630,7 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
|
|||
});
|
||||
|
||||
test('should assert value', async ({ openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
const { recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<input id=first value=foo>
|
||||
|
|
@ -679,7 +679,7 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
|
|||
test('should assert value on disabled input', async ({ openRecorder, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'pointerup event is not dispatched on a disabled input');
|
||||
|
||||
const recorder = await openRecorder();
|
||||
const { recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<input id=first value=foo>
|
||||
|
|
@ -702,7 +702,7 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
|
|||
});
|
||||
|
||||
test('should assert value on disabled select', async ({ openRecorder, browserName }) => {
|
||||
const recorder = await openRecorder();
|
||||
const { recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<select id=first><option value=foo1>Foo1</option><option value=bar1>Bar1</option></select>
|
||||
|
|
@ -723,7 +723,7 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
|
|||
});
|
||||
|
||||
test('should assert visibility', async ({ openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
const { recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input>`);
|
||||
|
||||
|
|
@ -741,10 +741,10 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
|
|||
});
|
||||
|
||||
test('should keep toolbar visible even if webpage erases content in hydration', async ({ openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
const { recorder } = await openRecorder();
|
||||
|
||||
const hydrate = () => {
|
||||
setTimeout(() => {
|
||||
window.builtinSetTimeout(() => {
|
||||
document.documentElement.innerHTML = '<p>Post-Hydration Content</p>';
|
||||
}, 500);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export { expect } from '@playwright/test';
|
|||
type CLITestArgs = {
|
||||
recorderPageGetter: () => Promise<Page>;
|
||||
closeRecorder: () => Promise<void>;
|
||||
openRecorder: (options?: { testIdAttributeName: string }) => Promise<Recorder>;
|
||||
openRecorder: (options?: { testIdAttributeName: string }) => Promise<{ recorder: Recorder, page: Page }>;
|
||||
runCLI: (args: string[], options?: { autoExitWhen?: string }) => CLIMock;
|
||||
};
|
||||
|
||||
|
|
@ -50,12 +50,11 @@ const playwrightToAutomateInspector = require('../../../packages/playwright-core
|
|||
|
||||
export const test = contextTest.extend<CLITestArgs>({
|
||||
recorderPageGetter: async ({ context, toImpl, mode }, run, testInfo) => {
|
||||
process.env.PWTEST_RECORDER_PORT = String(10907 + testInfo.workerIndex);
|
||||
testInfo.skip(mode.startsWith('service'));
|
||||
await run(async () => {
|
||||
while (!toImpl(context).recorderAppForTest)
|
||||
await new Promise(f => setTimeout(f, 100));
|
||||
const wsEndpoint = toImpl(context).recorderAppForTest.wsEndpoint;
|
||||
const wsEndpoint = toImpl(context).recorderAppForTest.wsEndpointForTest;
|
||||
const browser = await playwrightToAutomateInspector.chromium.connectOverCDP({ wsEndpoint });
|
||||
const c = browser.contexts()[0];
|
||||
return c.pages()[0] || await c.waitForEvent('page');
|
||||
|
|
@ -68,19 +67,32 @@ export const test = contextTest.extend<CLITestArgs>({
|
|||
});
|
||||
},
|
||||
|
||||
runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions }, run, testInfo) => {
|
||||
process.env.PWTEST_RECORDER_PORT = String(10907 + testInfo.workerIndex);
|
||||
runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions, codegenMode }, run, testInfo) => {
|
||||
testInfo.skip(mode.startsWith('service'));
|
||||
|
||||
await run((cliArgs, { autoExitWhen } = {}) => {
|
||||
return new CLIMock(childProcess, browserName, channel, headless, cliArgs, launchOptions.executablePath, autoExitWhen);
|
||||
return new CLIMock(childProcess, {
|
||||
browserName,
|
||||
channel,
|
||||
headless,
|
||||
args: cliArgs,
|
||||
executablePath: launchOptions.executablePath,
|
||||
autoExitWhen,
|
||||
codegenMode
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
openRecorder: async ({ page, recorderPageGetter }, run) => {
|
||||
openRecorder: async ({ context, recorderPageGetter, codegenMode }, run) => {
|
||||
await run(async (options?: { testIdAttributeName?: string }) => {
|
||||
await (page.context() as any)._enableRecorder({ language: 'javascript', mode: 'recording', ...options });
|
||||
return new Recorder(page, await recorderPageGetter());
|
||||
await (context as any)._enableRecorder({
|
||||
language: 'javascript',
|
||||
mode: 'recording',
|
||||
codegenMode,
|
||||
...options
|
||||
});
|
||||
const page = await context.newPage();
|
||||
return { page, recorder: new Recorder(page, await recorderPageGetter()) };
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -206,23 +218,24 @@ class Recorder {
|
|||
class CLIMock {
|
||||
process: TestChildProcess;
|
||||
|
||||
constructor(childProcess: CommonFixtures['childProcess'], browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined) {
|
||||
constructor(childProcess: CommonFixtures['childProcess'], options: { browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined, codegenMode?: 'trace-events' | 'actions'}) {
|
||||
const nodeArgs = [
|
||||
'node',
|
||||
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'),
|
||||
'codegen',
|
||||
...args,
|
||||
`--browser=${browserName}`,
|
||||
...options.args,
|
||||
`--browser=${options.browserName}`,
|
||||
];
|
||||
if (channel)
|
||||
nodeArgs.push(`--channel=${channel}`);
|
||||
if (options.channel)
|
||||
nodeArgs.push(`--channel=${options.channel}`);
|
||||
this.process = childProcess({
|
||||
command: nodeArgs,
|
||||
env: {
|
||||
PWTEST_CLI_AUTO_EXIT_WHEN: autoExitWhen,
|
||||
PW_RECORDER_IS_TRACE_VIEWER: options.codegenMode === 'trace-events' ? '1' : undefined,
|
||||
PWTEST_CLI_AUTO_EXIT_WHEN: options.autoExitWhen,
|
||||
PWTEST_CLI_IS_UNDER_TEST: '1',
|
||||
PWTEST_CLI_HEADLESS: headless ? '1' : undefined,
|
||||
PWTEST_CLI_EXECUTABLE_PATH: executablePath,
|
||||
PWTEST_CLI_HEADLESS: options.headless ? '1' : undefined,
|
||||
PWTEST_CLI_EXECUTABLE_PATH: options.executablePath,
|
||||
DEBUG: (process.env.DEBUG ?? '') + ',pw:browser*',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue