Merge branch 'main' of https://github.com/microsoft/playwright into settings-dialog-with-canvas-option

This commit is contained in:
Adam Gastineau 2024-12-13 08:18:18 -08:00
commit af7cc37e2a
99 changed files with 1316 additions and 488 deletions

View file

@ -234,6 +234,27 @@ jobs:
env:
PWTEST_CHANNEL: chromium-tip-of-tree
chromium_tot_headless_shell:
name: Chromium tip-of-tree headless-shell-${{ matrix.os }}
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04]
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: chromium-tip-of-tree-headless-shell
command: npm run ctest
bot-name: "chromium-tip-of-tree-headless-shell-${{ matrix.os }}"
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
env:
PWTEST_CHANNEL: chromium-tip-of-tree-headless-shell
firefox_beta:
name: Firefox Beta ${{ matrix.os }}
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}

View file

@ -35,7 +35,7 @@ Coding style is fully defined in [.eslintrc](https://github.com/microsoft/playwr
npm run lint
```
Comments should be generally avoided. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.
Comments should have an explicit purpose and should improve readability rather than hinder it. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.
### Write documentation

View file

@ -1,6 +1,6 @@
# 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-132.0.6834.32-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-132.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-132.0.6834.46-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-132.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->132.0.6834.32<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->132.0.6834.46<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->132.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |

View file

@ -1412,7 +1412,7 @@ This setting will change the default maximum time for all the methods accepting
* since: v1.8
- `timeout` <[float]>
Maximum time in milliseconds
Maximum time in milliseconds. Pass `0` to disable timeout.
## async method: BrowserContext.setExtraHTTPHeaders
* since: v1.8

View file

@ -161,6 +161,41 @@ await page.Clock.PauseAtAsync(DateTime.Parse("2020-02-02"));
await page.Clock.PauseAtAsync("2020-02-02");
```
For best results, install the clock before navigating the page and set it to a time slightly before the intended test time. This ensures that all timers run normally during page loading, preventing the page from getting stuck. Once the page has fully loaded, you can safely use [`method: Clock.pauseAt`] to pause the clock.
```js
// Initialize clock with some time before the test time and let the page load
// naturally. `Date.now` will progress as the timers fire.
await page.clock.install({ time: new Date('2024-12-10T08:00:00') });
await page.goto('http://localhost:3333');
await page.clock.pauseAt(new Date('2024-12-10T10:00:00'));
```
```python async
# Initialize clock with some time before the test time and let the page load
# naturally. `Date.now` will progress as the timers fire.
await page.clock.install(time=datetime.datetime(2024, 12, 10, 8, 0, 0))
await page.goto("http://localhost:3333")
await page.clock.pause_at(datetime.datetime(2024, 12, 10, 10, 0, 0))
```
```python sync
# Initialize clock with some time before the test time and let the page load
# naturally. `Date.now` will progress as the timers fire.
page.clock.install(time=datetime.datetime(2024, 12, 10, 8, 0, 0))
page.goto("http://localhost:3333")
page.clock.pause_at(datetime.datetime(2024, 12, 10, 10, 0, 0))
```
```java
// Initialize clock with some time before the test time and let the page load
// naturally. `Date.now` will progress as the timers fire.
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-12-10T08:00:00")));
page.navigate("http://localhost:3333");
page.clock().pauseAt(format.parse("2024-12-10T10:00:00"));
```
### param: Clock.pauseAt.time
* langs: js, java
* since: v1.45

View file

@ -155,7 +155,7 @@ Additional locator to match.
- returns: <[string]>
Captures the aria snapshot of the given element.
Read more about [aria snapshots](../aria-snapshots.md) and [`method: LocatorAssertions.toMatchAriaSnapshot`] for the corresponding assertion.
Read more about [aria snapshots](../aria-snapshots.md) and [`method: LocatorAssertions.toMatchAriaSnapshot#2`] for the corresponding assertion.
**Usage**

View file

@ -446,7 +446,7 @@ Expected options currently selected.
* since: v1.49
* langs: python
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot`].
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot#2`].
### param: LocatorAssertions.NotToMatchAriaSnapshot.expected
* since: v1.49
@ -2121,7 +2121,58 @@ Expected options currently selected.
* since: v1.23
## async method: LocatorAssertions.toMatchAriaSnapshot
## async method: LocatorAssertions.toMatchAriaSnapshot#1
* since: v1.50
* langs:
- alias-java: matchesAriaSnapshot
Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md).
**Usage**
```js
await expect(page.locator('body')).toMatchAriaSnapshot();
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
await expect(page.locator('body')).toMatchAriaSnapshot({ path: '/path/to/snapshot.yml' });
```
```python async
await expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
```
```python sync
expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
```
```csharp
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" });
```
```java
assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.MatchesAriaSnapshotOptions().setPath("/path/to/snapshot.yml"));
```
### option: LocatorAssertions.toMatchAriaSnapshot#1.name
* since: v1.50
* langs: js
- `name` <[string]>
Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not specified.
### option: LocatorAssertions.toMatchAriaSnapshot#1.path
* since: v1.50
- `path` <[string]>
Path to the YAML snapshot file.
### option: LocatorAssertions.toMatchAriaSnapshot#1.timeout = %%-js-assertions-timeout-%%
* since: v1.50
### option: LocatorAssertions.toMatchAriaSnapshot#1.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.50
## async method: LocatorAssertions.toMatchAriaSnapshot#2
* since: v1.49
* langs:
- alias-java: matchesAriaSnapshot
@ -2170,12 +2221,12 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
""");
```
### param: LocatorAssertions.toMatchAriaSnapshot.expected
### param: LocatorAssertions.toMatchAriaSnapshot#2.expected
* since: v1.49
- `expected` <string>
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%
* since: v1.49
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-csharp-java-python-assertions-timeout-%%
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.49

View file

@ -3970,7 +3970,7 @@ This setting will change the default maximum time for all the methods accepting
* since: v1.8
- `timeout` <[float]>
Maximum time in milliseconds
Maximum time in milliseconds. Pass `0` to disable timeout.
## async method: Page.setExtraHTTPHeaders
* since: v1.8

View file

@ -154,7 +154,7 @@ structure of a page, use the [Chrome DevTools Accessibility Pane](https://develo
## Snapshot matching
The [`method: LocatorAssertions.toMatchAriaSnapshot`] assertion method in Playwright compares the accessible
The [`method: LocatorAssertions.toMatchAriaSnapshot#2`] assertion method in Playwright compares the accessible
structure of the locator scope with a predefined aria snapshot template, helping validate the page's state against
testing requirements.

View file

@ -342,7 +342,7 @@ Playwright ships a regular Chromium build for headed operations and a separate [
#### Optimize download size on CI
If you are only running tests in headless mode, for example on CI, you can avoid downloading a regular version of Chromium by passing `--only-shell` during installation.
If you are only running tests in headless shell (i.e. the `channel` option is not specified), for example on CI, you can avoid downloading the full Chromium browser by passing `--only-shell` during installation.
```bash js
# only running tests headlessly

View file

@ -34,6 +34,10 @@ The recommended approach is to use `setFixedTime` to set the time to a specific
- `Event.timeStamp`
:::
:::warning
If you call `install` at any point in your test, the call _MUST_ occur before any other clock related calls (see note above for list). Calling these methods out of order will result in undefined behavior. For example, you cannot call `setInterval`, followed by `install`, then `clearInterval`, as `install` overrides the native definition of the clock functions.
:::
## Test with predefined time
Often you only need to fake `Date.now` while keeping the timers going.

View file

@ -103,6 +103,88 @@ Using `--ipc=host` is recommended when using Chrome ([Docker docs](https://docs.
See our [Continuous Integration guides](./ci.md) for sample configs.
### Remote Connection
You can run Playwright Server in Docker while keeping your tests running on the host system or another machine. This is useful for running tests on unsupported Linux distributions or remote execution scenarios.
#### Running the Playwright Server
Start the Playwright Server in Docker:
```bash
docker run -p 3000:3000 --rm --init -it --workdir /home/pwuser --user pwuser mcr.microsoft.com/playwright:v%%VERSION%%-noble /bin/sh -c "npx -y playwright@%%VERSION%% run-server --port 3000 --host 0.0.0.0"
```
#### Connecting to the Server
* langs: js
There are two ways to connect to the remote Playwright server:
1. Using environment variable with `@playwright/test`:
```bash
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:3000/ npx playwright test
```
2. Using the [`method: BrowserType.connect`] API for other applications:
```js
const browser = await playwright['chromium'].connect('ws://127.0.0.1:3000/');
```
#### Connecting to the Server
* langs: python, csharp, java
```python sync
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.connect("ws://127.0.0.1:3000/")
```
```python async
from playwright.async_api import async_playwright
async with async_playwright() as p:
browser = await p.chromium.connect("ws://127.0.0.1:3000/")
```
```csharp
using Microsoft.Playwright;
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.ConnectAsync("ws://127.0.0.1:3000/");
```
```java
package org.example;
import com.microsoft.playwright.*;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().connect("ws://127.0.0.1:3000/");
}
}
}
```
#### Network Configuration
If you need to access local servers from within the Docker container:
```bash
docker run --add-host=hostmachine:host-gateway -p 3000:3000 --rm --init -it --workdir /home/pwuser --user pwuser mcr.microsoft.com/playwright:v%%VERSION%%-noble /bin/sh -c "npx -y playwright@%%VERSION%% run-server --port 3000 --host 0.0.0.0"
```
This makes `hostmachine` point to the host's localhost. Your tests should use `hostmachine` instead of `localhost` when accessing local servers.
:::note
When running tests remotely, ensure the Playwright version in your tests matches the version running in the Docker container.
:::
## Image tags
See [all available image tags].

View file

@ -9,7 +9,7 @@ toc_max_heading_level: 2
### Aria snapshots
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#2`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
```csharp
await page.GotoAsync("https://playwright.dev");

View file

@ -8,7 +8,7 @@ toc_max_heading_level: 2
### Aria snapshots
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#2`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
```java
page.navigate("https://playwright.dev");

View file

@ -15,7 +15,7 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
### Aria snapshots
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#2`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
```js
await page.goto('https://playwright.dev');

View file

@ -8,7 +8,7 @@ toc_max_heading_level: 2
### Aria snapshots
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#2`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
```python
page.goto("https://playwright.dev")

View file

@ -37,7 +37,7 @@ When `fullyParallel: true` is enabled, Playwright Test runs individual tests in
**Sharding without fullyParallel**
Without the fullyParallel setting, Playwright Test defaults to file-level granularity, meaning entire test files are assigned to shards. In this case, the number of tests per file can greatly influence shard distribution. If your test files are not evenly sized (i.e., some files contain many more tests than others), certain shards may end up running significantly more tests, while others may run fewer or even none.
Without the fullyParallel setting, Playwright Test defaults to file-level granularity, meaning entire test files are assigned to shards (note that the same file may be assigned to different shards across different projects). In this case, the number of tests per file can greatly influence shard distribution. If your test files are not evenly sized (i.e., some files contain many more tests than others), certain shards may end up running significantly more tests, while others may run fewer or even none.
**Key Takeaways:**

258
package-lock.json generated
View file

@ -27,7 +27,7 @@
"@types/codemirror": "^5.60.7",
"@types/formidable": "^2.0.4",
"@types/immutable": "^3.8.7",
"@types/node": "^18.19.39",
"@types/node": "^18.19.68",
"@types/react": "^18.0.12",
"@types/react-dom": "^18.0.5",
"@types/ws": "^8.5.3",
@ -60,7 +60,7 @@
"react": "^18.1.0",
"react-dom": "^18.1.0",
"ssim.js": "^3.5.0",
"typescript": "^5.5.3",
"typescript": "^5.7.2",
"vite": "^5.4.6",
"ws": "^8.17.1",
"xml2js": "^0.5.0",
@ -397,17 +397,19 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
"integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@ -447,9 +449,13 @@
}
},
"node_modules/@babel/parser": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz",
"integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==",
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.3"
},
"bin": {
"parser": "bin/babel-parser.js"
},
@ -810,13 +816,13 @@
}
},
"node_modules/@babel/types": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz",
"integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==",
"version": "7.26.3",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
@ -1409,9 +1415,10 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.21",
@ -1851,10 +1858,11 @@
}
},
"node_modules/@types/node": {
"version": "18.19.39",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz",
"integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==",
"version": "18.19.68",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.68.tgz",
"integrity": "sha512-QGtpFH1vB99ZmTa63K4/FU8twThj4fuVSBkGddTp7uIL/cuoLWIUSL2RcOaigBhfR+hg5pgGkBnkoOxrTVBMKw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
@ -2184,126 +2192,126 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz",
"integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/parser": "^7.23.6",
"@vue/shared": "3.4.15",
"@babel/parser": "^7.25.3",
"@vue/shared": "3.5.13",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-core/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"peer": true,
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-core/node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT",
"peer": true
},
"node_modules/@vue/compiler-dom": {
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz",
"integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-core": "3.4.15",
"@vue/shared": "3.4.15"
"@vue/compiler-core": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz",
"integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/parser": "^7.23.6",
"@vue/compiler-core": "3.4.15",
"@vue/compiler-dom": "3.4.15",
"@vue/compiler-ssr": "3.4.15",
"@vue/shared": "3.4.15",
"@babel/parser": "^7.25.3",
"@vue/compiler-core": "3.5.13",
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.5.13",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.5",
"postcss": "^8.4.33",
"source-map-js": "^1.0.2"
"magic-string": "^0.30.11",
"postcss": "^8.4.48",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-sfc/node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT",
"peer": true
},
"node_modules/@vue/compiler-ssr": {
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz",
"integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.4.15",
"@vue/shared": "3.4.15"
"@vue/compiler-dom": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/reactivity": {
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz",
"integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/shared": "3.4.15"
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz",
"integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.4.15",
"@vue/shared": "3.4.15"
"@vue/reactivity": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz",
"integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/runtime-core": "3.4.15",
"@vue/shared": "3.4.15",
"@vue/reactivity": "3.5.13",
"@vue/runtime-core": "3.5.13",
"@vue/shared": "3.5.13",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz",
"integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.4.15",
"@vue/shared": "3.4.15"
"@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.5.13"
},
"peerDependencies": {
"vue": "3.4.15"
"vue": "3.5.13"
}
},
"node_modules/@vue/shared": {
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
"integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"license": "MIT",
"peer": true
},
"node_modules/@zip.js/zip.js": {
@ -3326,6 +3334,19 @@
"once": "^1.4.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"peer": true,
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
@ -5236,14 +5257,12 @@
}
},
"node_modules/magic-string": {
"version": "0.30.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
"version": "0.30.15",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.15.tgz",
"integrity": "sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/make-dir": {
@ -5396,15 +5415,16 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@ -5772,9 +5792,10 @@
}
},
"node_modules/picocolors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@ -5827,9 +5848,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"funding": [
{
"type": "opencollective",
@ -5844,9 +5865,10 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.0",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
@ -6720,14 +6742,6 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"engines": {
"node": ">=4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -6888,10 +6902,11 @@
}
},
"node_modules/typescript": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -7450,16 +7465,17 @@
}
},
"node_modules/vue": {
"version": "3.4.15",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz",
"integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==",
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.4.15",
"@vue/compiler-sfc": "3.4.15",
"@vue/runtime-dom": "3.4.15",
"@vue/server-renderer": "3.4.15",
"@vue/shared": "3.4.15"
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-sfc": "3.5.13",
"@vue/runtime-dom": "3.5.13",
"@vue/server-renderer": "3.5.13",
"@vue/shared": "3.5.13"
},
"peerDependencies": {
"typescript": "*"

View file

@ -66,7 +66,7 @@
"@types/codemirror": "^5.60.7",
"@types/formidable": "^2.0.4",
"@types/immutable": "^3.8.7",
"@types/node": "^18.19.39",
"@types/node": "^18.19.68",
"@types/react": "^18.0.12",
"@types/react-dom": "^18.0.5",
"@types/ws": "^8.5.3",
@ -99,7 +99,7 @@
"react": "^18.1.0",
"react-dom": "^18.1.0",
"ssim.js": "^3.5.0",
"typescript": "^5.5.3",
"typescript": "^5.7.2",
"vite": "^5.4.6",
"ws": "^8.17.1",
"xml2js": "^0.5.0",

View file

@ -2,10 +2,5 @@
"name": "html-reporter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && tsc",
"preview": "vite preview"
}
"type": "module"
}

View file

@ -64,6 +64,6 @@ test('should toggle filters', async ({ page, mount }) => {
await expect(page).toHaveURL(/#\?q=s:flaky/);
await component.locator('a', { hasText: 'Skipped' }).click();
await expect(page).toHaveURL(/#\?q=s:skipped/);
await component.getByRole('searchbox').fill('annot:annotation type=annotation description');
await component.getByRole('textbox').fill('annot:annotation type=annotation description');
expect(filters).toEqual(['', 's:passed', 's:failed', 's:flaky', 's:skipped', 'annot:annotation type=annotation description']);
});

View file

@ -49,12 +49,14 @@ export const HeaderView: React.FC<React.PropsWithChildren<{
<form className='subnav-search' onSubmit={
event => {
event.preventDefault();
navigate(`#?q=${filterText ? encodeURIComponent(filterText) : ''}`);
const url = new URL(window.location.href);
url.hash = filterText ? '?' + new URLSearchParams({ q: filterText }) : '';
navigate(url);
}
}>
{icons.search()}
{/* Use navigationId to reset defaultValue */}
<input type='search' spellCheck={false} className='form-control subnav-search-input input-contrast width-full' value={filterText} onChange={e => {
<input spellCheck={false} className='form-control subnav-search-input input-contrast width-full' value={filterText} onChange={e => {
setFilterText(e.target.value);
}}></input>
</form>

View file

@ -15,7 +15,7 @@
*/
import type { HTMLReport } from './types';
import type zip from '@zip.js/zip.js';
import type * as zip from '@zip.js/zip.js';
// @ts-ignore
import * as zipImport from '@zip.js/zip.js/lib/zip-no-worker-inflate.js';
import * as React from 'react';

View file

@ -23,7 +23,7 @@ import './links.css';
import { linkifyText } from '@web/renderUtils';
import { clsx } from '@web/uiUtils';
export function navigate(href: string) {
export function navigate(href: string | URL) {
window.history.pushState({}, '', href);
const navEvent = new PopStateEvent('popstate');
window.dispatchEvent(navEvent);

View file

@ -6,7 +6,7 @@ This project incorporates components from the projects listed below. The origina
- @types/node@17.0.24 (https://github.com/DefinitelyTyped/DefinitelyTyped)
- @types/yauzl@2.10.0 (https://github.com/DefinitelyTyped/DefinitelyTyped)
- agent-base@7.1.1 (https://github.com/TooTallNate/proxy-agents)
- agent-base@7.1.3 (https://github.com/TooTallNate/proxy-agents)
- balanced-match@1.0.2 (https://github.com/juliangruber/balanced-match)
- brace-expansion@1.1.11 (https://github.com/juliangruber/brace-expansion)
- buffer-crc32@0.2.13 (https://github.com/brianloveswords/buffer-crc32)
@ -24,7 +24,7 @@ This project incorporates components from the projects listed below. The origina
- fd-slicer@1.1.0 (https://github.com/andrewrk/node-fd-slicer)
- get-stream@5.2.0 (https://github.com/sindresorhus/get-stream)
- graceful-fs@4.2.10 (https://github.com/isaacs/node-graceful-fs)
- https-proxy-agent@7.0.5 (https://github.com/TooTallNate/proxy-agents)
- https-proxy-agent@7.0.6 (https://github.com/TooTallNate/proxy-agents)
- ip-address@9.0.5 (https://github.com/beaugunderson/ip-address)
- is-docker@2.2.1 (https://github.com/sindresorhus/is-docker)
- is-wsl@2.2.0 (https://github.com/sindresorhus/is-wsl)
@ -43,7 +43,7 @@ This project incorporates components from the projects listed below. The origina
- retry@0.12.0 (https://github.com/tim-kos/node-retry)
- signal-exit@3.0.7 (https://github.com/tapjs/signal-exit)
- smart-buffer@4.2.0 (https://github.com/JoshGlazebrook/smart-buffer)
- socks-proxy-agent@8.0.4 (https://github.com/TooTallNate/proxy-agents)
- socks-proxy-agent@8.0.5 (https://github.com/TooTallNate/proxy-agents)
- socks@2.8.3 (https://github.com/JoshGlazebrook/socks)
- sprintf-js@1.1.3 (https://github.com/alexei/sprintf.js)
- stack-utils@2.0.5 (https://github.com/tapjs/stack-utils)
@ -105,7 +105,7 @@ MIT License
=========================================
END OF @types/yauzl@2.10.0 AND INFORMATION
%% agent-base@7.1.1 NOTICES AND INFORMATION BEGIN HERE
%% agent-base@7.1.3 NOTICES AND INFORMATION BEGIN HERE
=========================================
(The MIT License)
@ -130,7 +130,7 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
=========================================
END OF agent-base@7.1.1 AND INFORMATION
END OF agent-base@7.1.3 AND INFORMATION
%% balanced-match@1.0.2 NOTICES AND INFORMATION BEGIN HERE
=========================================
@ -542,7 +542,7 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
=========================================
END OF graceful-fs@4.2.10 AND INFORMATION
%% https-proxy-agent@7.0.5 NOTICES AND INFORMATION BEGIN HERE
%% https-proxy-agent@7.0.6 NOTICES AND INFORMATION BEGIN HERE
=========================================
(The MIT License)
@ -567,7 +567,7 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
=========================================
END OF https-proxy-agent@7.0.5 AND INFORMATION
END OF https-proxy-agent@7.0.6 AND INFORMATION
%% ip-address@9.0.5 NOTICES AND INFORMATION BEGIN HERE
=========================================
@ -1005,7 +1005,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
=========================================
END OF smart-buffer@4.2.0 AND INFORMATION
%% socks-proxy-agent@8.0.4 NOTICES AND INFORMATION BEGIN HERE
%% socks-proxy-agent@8.0.5 NOTICES AND INFORMATION BEGIN HERE
=========================================
(The MIT License)
@ -1030,7 +1030,7 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
=========================================
END OF socks-proxy-agent@8.0.4 AND INFORMATION
END OF socks-proxy-agent@8.0.5 AND INFORMATION
%% socks@2.8.3 NOTICES AND INFORMATION BEGIN HERE
=========================================

View file

@ -3,21 +3,15 @@
"browsers": [
{
"name": "chromium",
"revision": "1151",
"revision": "1152",
"installByDefault": true,
"browserVersion": "132.0.6834.32"
},
{
"name": "chromium-headless-shell",
"revision": "1151",
"installByDefault": true,
"browserVersion": "132.0.6834.32"
"browserVersion": "132.0.6834.46"
},
{
"name": "chromium-tip-of-tree",
"revision": "1284",
"revision": "1286",
"installByDefault": false,
"browserVersion": "133.0.6878.0"
"browserVersion": "133.0.6891.0"
},
{
"name": "firefox",
@ -33,7 +27,7 @@
},
{
"name": "webkit",
"revision": "2113",
"revision": "2119",
"installByDefault": true,
"revisionOverrides": {
"debian11-x64": "2105",

View file

@ -14,7 +14,7 @@
"diff": "^7.0.0",
"dotenv": "^16.4.5",
"graceful-fs": "4.2.10",
"https-proxy-agent": "7.0.5",
"https-proxy-agent": "7.0.6",
"jpeg-js": "0.4.4",
"mime": "^3.0.0",
"minimatch": "^3.1.2",
@ -24,7 +24,7 @@
"proxy-from-env": "1.1.0",
"retry": "0.12.0",
"signal-exit": "3.0.7",
"socks-proxy-agent": "8.0.4",
"socks-proxy-agent": "8.0.5",
"stack-utils": "2.0.5",
"ws": "8.17.1",
"yaml": "^2.6.0"
@ -140,13 +140,10 @@
}
},
"node_modules/agent-base": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
@ -244,12 +241,12 @@
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
},
"node_modules/https-proxy-agent": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
"integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.0.2",
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
@ -403,12 +400,12 @@
}
},
"node_modules/socks-proxy-agent": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz",
"integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==",
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.1",
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"socks": "^2.8.3"
},
@ -562,12 +559,9 @@
}
},
"agent-base": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"requires": {
"debug": "^4.3.4"
}
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="
},
"balanced-match": {
"version": "1.0.2",
@ -632,11 +626,11 @@
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
},
"https-proxy-agent": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
"integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"requires": {
"agent-base": "^7.0.2",
"agent-base": "^7.1.2",
"debug": "4"
}
},
@ -740,11 +734,11 @@
}
},
"socks-proxy-agent": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz",
"integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==",
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
"requires": {
"agent-base": "^7.1.1",
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"socks": "^2.8.3"
}

View file

@ -15,7 +15,7 @@
"diff": "^7.0.0",
"dotenv": "^16.4.5",
"graceful-fs": "4.2.10",
"https-proxy-agent": "7.0.5",
"https-proxy-agent": "7.0.6",
"jpeg-js": "0.4.4",
"mime": "^3.0.0",
"minimatch": "^3.1.2",
@ -25,7 +25,7 @@
"proxy-from-env": "1.1.0",
"retry": "0.12.0",
"signal-exit": "3.0.7",
"socks-proxy-agent": "8.0.4",
"socks-proxy-agent": "8.0.5",
"stack-utils": "2.0.5",
"ws": "8.17.1",
"yaml": "^2.6.0"

View file

@ -38,7 +38,7 @@ export class AndroidServerLauncherImpl {
if (options.deviceSerialNumber) {
devices = devices.filter(d => d.serial === options.deviceSerialNumber);
if (devices.length === 0)
throw new Error(`No device with serial number '${options.deviceSerialNumber}' not found`);
throw new Error(`No device with serial number '${options.deviceSerialNumber}' was found`);
}
if (devices.length > 1)

View file

@ -31,7 +31,6 @@ import type { Browser } from '../client/browser';
import type { Page } from '../client/page';
import type { BrowserType } from '../client/browserType';
import type { BrowserContextOptions, LaunchOptions } from '../client/types';
import { spawn } from 'child_process';
import { wrapInASCIIBox, isLikelyNpxGlobal, assert, gracefullyProcessExitDoNotHang, getPackageManagerExecCommand } from '../utils';
import type { Executable } from '../server';
import { registry, writeDockerVersion } from '../server';
@ -77,21 +76,6 @@ Examples:
$ codegen --target=python
$ codegen -b webkit https://example.com`);
program
.command('debug <app> [args...]', { hidden: true })
.description('run command in debug mode: disable timeout, open inspector')
.allowUnknownOption(true)
.action(function(app, options) {
spawn(app, options, {
env: { ...process.env, PWDEBUG: '1' },
stdio: 'inherit'
});
}).addHelpText('afterAll', `
Examples:
$ debug node test.js
$ debug npm run test`);
function suggestedBrowsersToInstall() {
return registry.executables().filter(e => e.installType !== 'none' && e.type !== 'tool').map(e => e.name).join(', ');
}
@ -291,7 +275,7 @@ program
});
program
.command('run-server', { hidden: true })
.command('run-server')
.option('--port <port>', 'Server port')
.option('--host <host>', 'Server host')
.option('--path <path>', 'Endpoint Path', '/')

View file

@ -103,7 +103,7 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
async firstWindow(options?: { timeout?: number }): Promise<Page> {
if (this._windows.size)
return this._windows.values().next().value;
return this._windows.values().next().value!;
return await this.waitForEvent('window', options);
}

View file

@ -385,6 +385,7 @@ scheme.DebugControllerInitializer = tOptional(tObject({}));
scheme.DebugControllerInspectRequestedEvent = tObject({
selector: tString,
locator: tString,
ariaSnapshot: tString,
});
scheme.DebugControllerSetModeRequestedEvent = tObject({
mode: tString,

View file

@ -211,6 +211,10 @@ export class BidiBrowserContext extends BrowserContext {
return this._bidiPages().map(bidiPage => bidiPage._initializedPage).filter(Boolean) as Page[];
}
pagesOrErrors() {
return this._bidiPages().map(bidiPage => bidiPage.pageOrError());
}
async newPageDelegate(): Promise<PageDelegate> {
assertBrowserContextIsNotOwned(this);
const { context } = await this._browser._browserSession.send('browsingContext.create', {

View file

@ -259,6 +259,7 @@ export abstract class BrowserContext extends SdkObject {
// BrowserContext methods.
abstract pages(): Page[];
abstract pagesOrErrors(): Promise<Page | Error>[];
abstract newPageDelegate(): Promise<PageDelegate>;
abstract addCookies(cookies: channels.SetNetworkCookie[]): Promise<void>;
abstract setGeolocation(geolocation?: types.Geolocation): Promise<void>;
@ -358,29 +359,36 @@ export abstract class BrowserContext extends SdkObject {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async _loadDefaultContextAsIs(progress: Progress): Promise<Page[]> {
if (!this.pages().length) {
async _loadDefaultContextAsIs(progress: Progress): Promise<Page> {
let pageOrError;
if (!this.pagesOrErrors().length) {
const waitForEvent = helper.waitForEvent(progress, this, BrowserContext.Events.Page);
progress.cleanupWhenAborted(() => waitForEvent.dispose);
const page = (await waitForEvent.promise) as Page;
if (page._pageIsError)
throw page._pageIsError;
// Race against BrowserContext.close
pageOrError = await Promise.race([
waitForEvent.promise as Promise<Page>,
this._closePromise,
]);
// Consider Page initialization errors
if (pageOrError instanceof Page)
pageOrError = await pageOrError._delegate.pageOrError();
} else {
pageOrError = await this.pagesOrErrors()[0];
}
const pages = this.pages();
if (pages[0]._pageIsError)
throw pages[0]._pageIsError;
await pages[0].mainFrame()._waitForLoadState(progress, 'load');
return pages;
if (pageOrError instanceof Error)
throw pageOrError;
await pageOrError.mainFrame()._waitForLoadState(progress, 'load');
return pageOrError;
}
async _loadDefaultContext(progress: Progress) {
const pages = await this._loadDefaultContextAsIs(progress);
const defaultPage = await this._loadDefaultContextAsIs(progress);
const browserName = this._browser.options.name;
if ((this._options.isMobile && browserName === 'chromium') || (this._options.locale && browserName === 'webkit')) {
// Workaround for:
// - chromium fails to change isMobile for existing page;
// - webkit fails to change locale for existing page.
const oldPage = pages[0];
const oldPage = defaultPage;
await this.newPage(progress.metadata);
await oldPage.close(progress.metadata);
}

View file

@ -299,6 +299,6 @@ class CRAXNode implements accessibility.AXNode {
for (const childId of node._payload.childIds || [])
node._children.push(nodeById.get(childId)!);
}
return nodeById.values().next().value;
return nodeById.values().next().value!;
}
}

View file

@ -368,6 +368,10 @@ export class CRBrowserContext extends BrowserContext {
return this._crPages().map(crPage => crPage._initializedPage).filter(Boolean) as Page[];
}
pagesOrErrors() {
return this._crPages().map(crPage => crPage.pageOrError());
}
async newPageDelegate(): Promise<PageDelegate> {
assertBrowserContextIsNotOwned(this);

View file

@ -432,6 +432,9 @@ class FrameSession {
this._firstNonInitialNavigationCommittedFulfill = f;
this._firstNonInitialNavigationCommittedReject = r;
});
// The Promise is not always awaited (e.g. FrameSession._initialize can throw)
// so we catch errors here to prevent unhandled promise rejection.
this._firstNonInitialNavigationCommittedPromise.catch(() => {});
}
_isMainFrame(): boolean {

View file

@ -228,7 +228,7 @@ class InspectingRecorderApp extends EmptyRecorderApp {
override async elementPicked(elementInfo: ElementInfo): Promise<void> {
const locator: string = asLocator(this._debugController._sdkLanguage, elementInfo.selector);
this._debugController.emit(DebugController.Events.InspectRequested, { selector: elementInfo.selector, locator });
this._debugController.emit(DebugController.Events.InspectRequested, { selector: elementInfo.selector, locator, ariaSnapshot: elementInfo.ariaSnapshot });
}
override async setSources(sources: Source[]): Promise<void> {

View file

@ -110,7 +110,7 @@
"defaultBrowserType": "webkit"
},
"Galaxy S5": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 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/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Safari/537.36",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Safari/537.36 Edg/132.0.6834.32",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36 Edg/132.0.6834.46",
"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/132.0.6834.32 Safari/537.36",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 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/132.0.6834.32 Safari/537.36 Edg/132.0.6834.32",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.46 Safari/537.36 Edg/132.0.6834.46",
"screen": {
"width": 1920,
"height": 1080

View file

@ -32,8 +32,8 @@ export class DebugControllerDispatcher extends Dispatcher<DebugController, chann
eventsHelper.addEventListener(this._object, DebugController.Events.StateChanged, params => {
this._dispatchEvent('stateChanged', params);
}),
eventsHelper.addEventListener(this._object, DebugController.Events.InspectRequested, ({ selector, locator }) => {
this._dispatchEvent('inspectRequested', { selector, locator });
eventsHelper.addEventListener(this._object, DebugController.Events.InspectRequested, ({ selector, locator, ariaSnapshot }) => {
this._dispatchEvent('inspectRequested', { selector, locator, ariaSnapshot });
}),
eventsHelper.addEventListener(this._object, DebugController.Events.SourceChanged, ({ text, header, footer, actions }) => {
this._dispatchEvent('sourceChanged', ({ text, header, footer, actions }));

View file

@ -271,6 +271,10 @@ export class FFBrowserContext extends BrowserContext {
return this._ffPages().map(ffPage => ffPage._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[];
}
pagesOrErrors() {
return this._ffPages().map(ffPage => ffPage.pageOrError());
}
async newPageDelegate(): Promise<PageDelegate> {
assertBrowserContextIsNotOwned(this);
const { targetId } = await this._browser.session.send('Browser.newPage', {

View file

@ -158,8 +158,10 @@ function toAriaNode(element: Element): AriaNode | null {
if (roleUtils.kAriaSelectedRoles.includes(role))
result.selected = roleUtils.getAriaSelected(element);
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)
result.children = [element.value];
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
if (element.type !== 'checkbox' && element.type !== 'radio')
result.children = [element.value];
}
return result;
}

View file

@ -38,9 +38,9 @@ const kOtherTestIdScore = 2; // other data-test* attributes
const kIframeByAttributeScore = 10;
const kBeginPenalizedScore = 50;
const kPlaceholderScore = 100;
const kLabelScore = 120;
const kRoleWithNameScore = 140;
const kRoleWithNameScore = 100;
const kPlaceholderScore = 120;
const kLabelScore = 140;
const kAltTextScore = 160;
const kTextScore = 180;
const kTitleScore = 200;
@ -268,7 +268,7 @@ function buildNoTextCandidates(injectedScript: InjectedScript, element: Element,
if (input.placeholder) {
candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder, true)}]`, score: kPlaceholderScoreExact });
for (const alternative of suitableTextAlternatives(input.placeholder))
candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(alternative.text, false)}]`, score: kPlaceholderScore - alternative.scoreBouns });
candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(alternative.text, false)}]`, score: kPlaceholderScore - alternative.scoreBonus });
}
}
@ -277,7 +277,7 @@ function buildNoTextCandidates(injectedScript: InjectedScript, element: Element,
const labelText = label.normalized;
candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(labelText, true), score: kLabelScoreExact });
for (const alternative of suitableTextAlternatives(labelText))
candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(alternative.text, false), score: kLabelScore - alternative.scoreBouns });
candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(alternative.text, false), score: kLabelScore - alternative.scoreBonus });
}
const ariaRole = getAriaRole(element);
@ -308,28 +308,28 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
if (title) {
candidates.push([{ engine: 'internal:attr', selector: `[title=${escapeForAttributeSelector(title, true)}]`, score: kTitleScoreExact }]);
for (const alternative of suitableTextAlternatives(title))
candidates.push([{ engine: 'internal:attr', selector: `[title=${escapeForAttributeSelector(alternative.text, false)}]`, score: kTitleScore - alternative.scoreBouns }]);
candidates.push([{ engine: 'internal:attr', selector: `[title=${escapeForAttributeSelector(alternative.text, false)}]`, score: kTitleScore - alternative.scoreBonus }]);
}
const alt = element.getAttribute('alt');
if (alt && ['APPLET', 'AREA', 'IMG', 'INPUT'].includes(element.nodeName)) {
candidates.push([{ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(alt, true)}]`, score: kAltTextScoreExact }]);
for (const alternative of suitableTextAlternatives(alt))
candidates.push([{ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(alternative.text, false)}]`, score: kAltTextScore - alternative.scoreBouns }]);
candidates.push([{ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(alternative.text, false)}]`, score: kAltTextScore - alternative.scoreBonus }]);
}
const text = elementText(injectedScript._evaluator._cacheText, element).normalized;
const textAlternatives = text ? suitableTextAlternatives(text) : [];
if (text) {
const alternatives = suitableTextAlternatives(text);
if (isTargetNode) {
if (text.length <= 80)
candidates.push([{ engine: 'internal:text', selector: escapeForTextSelector(text, true), score: kTextScoreExact }]);
for (const alternative of alternatives)
candidates.push([{ engine: 'internal:text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBouns }]);
for (const alternative of textAlternatives)
candidates.push([{ engine: 'internal:text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBonus }]);
}
const cssToken: SelectorToken = { engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: kCSSTagNameScore };
for (const alternative of alternatives)
candidates.push([cssToken, { engine: 'internal:has-text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBouns }]);
for (const alternative of textAlternatives)
candidates.push([cssToken, { engine: 'internal:has-text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBonus }]);
if (text.length <= 80) {
const re = new RegExp('^' + escapeRegExp(text) + '$');
candidates.push([cssToken, { engine: 'internal:has-text', selector: escapeForTextSelector(re, false), score: kTextScoreRegex }]);
@ -340,9 +340,18 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
if (ariaRole && !['none', 'presentation'].includes(ariaRole)) {
const ariaName = getElementAccessibleName(element, false);
if (ariaName) {
candidates.push([{ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScoreExact }]);
const roleToken = { engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScoreExact };
candidates.push([roleToken]);
for (const alternative of suitableTextAlternatives(ariaName))
candidates.push([{ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(alternative.text, false)}]`, score: kRoleWithNameScore - alternative.scoreBouns }]);
candidates.push([{ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(alternative.text, false)}]`, score: kRoleWithNameScore - alternative.scoreBonus }]);
} else {
const roleToken = { engine: 'internal:role', selector: `${ariaRole}`, score: kRoleWithoutNameScore };
for (const alternative of textAlternatives)
candidates.push([roleToken, { engine: 'internal:has-text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBonus }]);
if (text.length <= 80) {
const re = new RegExp('^' + escapeRegExp(text) + '$');
candidates.push([roleToken, { engine: 'internal:has-text', selector: escapeForTextSelector(re, false), score: kTextScoreRegex }]);
}
}
}
@ -527,14 +536,14 @@ function trimWordBoundary(text: string, maxLength: number) {
}
function suitableTextAlternatives(text: string) {
let result: { text: string, scoreBouns: number }[] = [];
let result: { text: string, scoreBonus: number }[] = [];
{
const match = text.match(/^([\d.,]+)[^.,\w]/);
const leadingNumberLength = match ? match[1].length : 0;
if (leadingNumberLength) {
const alt = trimWordBoundary(text.substring(leadingNumberLength).trimStart(), 80);
result.push({ text: alt, scoreBouns: alt.length <= 30 ? 2 : 1 });
result.push({ text: alt, scoreBonus: alt.length <= 30 ? 2 : 1 });
}
}
@ -543,20 +552,20 @@ function suitableTextAlternatives(text: string) {
const trailingNumberLength = match ? match[1].length : 0;
if (trailingNumberLength) {
const alt = trimWordBoundary(text.substring(0, text.length - trailingNumberLength).trimEnd(), 80);
result.push({ text: alt, scoreBouns: alt.length <= 30 ? 2 : 1 });
result.push({ text: alt, scoreBonus: alt.length <= 30 ? 2 : 1 });
}
}
if (text.length <= 30) {
result.push({ text, scoreBouns: 0 });
result.push({ text, scoreBonus: 0 });
} else {
result.push({ text: trimWordBoundary(text, 80), scoreBouns: 0 });
result.push({ text: trimWordBoundary(text, 30), scoreBouns: 1 });
result.push({ text: trimWordBoundary(text, 80), scoreBonus: 0 });
result.push({ text: trimWordBoundary(text, 30), scoreBonus: 1 });
}
result = result.filter(r => r.text);
if (!result.length)
result.push({ text: text.substring(0, 80), scoreBouns: 0 });
result.push({ text: text.substring(0, 80), scoreBonus: 0 });
return result;
}

View file

@ -73,6 +73,8 @@ export function elementText(cache: Map<Element | ShadowRoot, ElementText>, root:
if (child.nodeType === Node.TEXT_NODE) {
value.full += child.nodeValue || '';
currentImmediate += child.nodeValue || '';
} else if (child.nodeType === Node.COMMENT_NODE) {
continue;
} else {
if (currentImmediate)
value.immediate.push(currentImmediate);

View file

@ -164,7 +164,6 @@ export class Page extends SdkObject {
_clientRequestInterceptor: network.RouteHandler | undefined;
_serverRequestInterceptor: network.RouteHandler | undefined;
_ownedContext: BrowserContext | undefined;
_pageIsError: Error | undefined;
_video: Artifact | null = null;
_opener: Page | undefined;
private _isServerSideOnly = false;
@ -208,7 +207,7 @@ export class Page extends SdkObject {
// context/browser closure. Just ignore the page.
if (this._browserContext.isClosingOrClosed())
return;
this._setIsError(error);
this._frameManager.createDummyMainFrameIfNeeded();
}
this._initialized = true;
this.emitOnContext(contextEvent, this);
@ -709,11 +708,6 @@ export class Page extends SdkObject {
await this._ownedContext.close(options);
}
private _setIsError(error: Error) {
this._pageIsError = error;
this._frameManager.createDummyMainFrameIfNeeded();
}
isClosed(): boolean {
return this._closedState === 'closed';
}

View file

@ -38,7 +38,7 @@ export class RecorderCollection extends EventEmitter {
restart() {
this._actions = [];
this._fireChange();
this.emit('change', []);
}
setEnabled(enabled: boolean) {
@ -128,6 +128,7 @@ export class RecorderCollection extends EventEmitter {
private _fireChange() {
if (!this._enabled)
return;
this.emit('change', collapseActions(this._actions));
}
}

View file

@ -174,6 +174,35 @@ const DOWNLOAD_PATHS: Record<BrowserName | InternalTool, DownloadPaths> = {
'mac15-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-mac-arm64.zip',
'win64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-win64.zip',
},
'chromium-tip-of-tree-headless-shell': {
'<unknown>': undefined,
'ubuntu18.04-x64': undefined,
'ubuntu20.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'ubuntu22.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'ubuntu24.04-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'ubuntu18.04-arm64': undefined,
'ubuntu20.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'ubuntu22.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'ubuntu24.04-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'debian11-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'debian11-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'debian12-x64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux.zip',
'debian12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-linux-arm64.zip',
'mac10.13': undefined,
'mac10.14': undefined,
'mac10.15': undefined,
'mac11': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac11-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'mac12': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac12-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'mac13': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac13-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'mac14': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac14-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'mac15': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac.zip',
'mac15-arm64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-mac-arm64.zip',
'win64': 'builds/chromium-tip-of-tree/%s/chromium-tip-of-tree-headless-shell-win64.zip',
},
'firefox': {
'<unknown>': undefined,
'ubuntu18.04-x64': undefined,
@ -386,7 +415,14 @@ type BrowsersJSONDescriptor = {
};
function readDescriptors(browsersJSON: BrowsersJSON): BrowsersJSONDescriptor[] {
return (browsersJSON['browsers']).map(obj => {
const headlessShells: BrowsersJSON['browsers'] = [];
for (const browserName of ['chromium', 'chromium-tip-of-tree']) {
headlessShells.push({
...browsersJSON.browsers.find(browser => browser.name === browserName)!,
name: `${browserName}-headless-shell`,
});
}
return [...browsersJSON.browsers, ...headlessShells].map(obj => {
const name = obj.name;
const revisionOverride = (obj.revisionOverrides || {})[hostPlatform];
const revision = revisionOverride || obj.revision;
@ -410,10 +446,10 @@ function readDescriptors(browsersJSON: BrowsersJSON): BrowsersJSONDescriptor[] {
}
export type BrowserName = 'chromium' | 'firefox' | 'webkit' | 'bidi';
type InternalTool = 'ffmpeg' | 'firefox-beta' | 'chromium-tip-of-tree' | 'chromium-headless-shell' | 'android';
type InternalTool = 'ffmpeg' | 'firefox-beta' | 'chromium-tip-of-tree' | 'chromium-headless-shell' | 'chromium-tip-of-tree-headless-shell' | 'android';
type BidiChannel = 'bidi-firefox-stable' | 'bidi-firefox-beta' | 'bidi-firefox-nightly' | 'bidi-chrome-canary' | 'bidi-chrome-stable' | 'bidi-chromium';
type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary';
const allDownloadable = ['android', 'chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree', 'chromium-headless-shell'];
const allDownloadable = ['android', 'chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree', 'chromium-headless-shell', 'chromium-tip-of-tree-headless-shell'];
export interface Executable {
type: 'browser' | 'tool' | 'channel';
@ -514,6 +550,24 @@ export class Registry {
_isHermeticInstallation: true,
});
const chromiumTipOfTreeHeadlessShell = descriptors.find(d => d.name === 'chromium-tip-of-tree-headless-shell')!;
const chromiumTipOfTreeHeadlessShellExecutable = findExecutablePath(chromiumTipOfTreeHeadlessShell.dir, 'chromium-headless-shell');
this._executables.push({
type: 'channel',
name: 'chromium-tip-of-tree-headless-shell',
browserName: 'chromium',
directory: chromiumTipOfTreeHeadlessShell.dir,
executablePath: () => chromiumTipOfTreeHeadlessShellExecutable,
executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium', chromiumTipOfTreeHeadlessShellExecutable, chromiumTipOfTreeHeadlessShell.installByDefault, sdkLanguage),
installType: chromiumTipOfTreeHeadlessShell.installByDefault ? 'download-by-default' : 'download-on-demand',
_validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, chromiumTipOfTreeHeadlessShell.dir, ['chrome-linux'], [], ['chrome-win']),
downloadURLs: this._downloadURLs(chromiumTipOfTreeHeadlessShell),
browserVersion: chromium.browserVersion,
_install: () => this._downloadExecutable(chromiumTipOfTreeHeadlessShell, chromiumTipOfTreeHeadlessShellExecutable),
_dependencyGroup: 'chromium',
_isHermeticInstallation: true,
});
const chromiumTipOfTree = descriptors.find(d => d.name === 'chromium-tip-of-tree')!;
const chromiumTipOfTreeExecutable = findExecutablePath(chromiumTipOfTree.dir, 'chromium');
this._executables.push({

View file

@ -438,13 +438,13 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
expectValue(value);
attrs[kSelectedAttribute] = value;
}
if (nodeName === 'CANVAS') {
const boundingRect = (element as HTMLCanvasElement).getBoundingClientRect();
if (nodeName === 'CANVAS' || nodeName === 'IFRAME' || nodeName === 'FRAME') {
const boundingRect = (element as HTMLElement).getBoundingClientRect();
const value = JSON.stringify({
left: boundingRect.left / window.innerWidth,
top: boundingRect.top / window.innerHeight,
right: boundingRect.right / window.innerWidth,
bottom: boundingRect.bottom / window.innerHeight
left: boundingRect.left,
top: boundingRect.top,
right: boundingRect.right,
bottom: boundingRect.bottom
});
expectValue(kBoundingRectAttribute);
expectValue(value);

View file

@ -125,7 +125,13 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[
for (const reporter of options.reporter || [])
params.append('reporter', reporter);
const urlPath = `./trace/${options.webApp || 'index.html'}?${params.toString()}`;
let baseUrl = '.';
if (process.env.PW_HMR) {
baseUrl = 'http://localhost:44223'; // port is hardcoded in build.js
params.set('server', server.urlPrefix('precise'));
}
const urlPath = `${baseUrl}/trace/${options.webApp || 'index.html'}?${params.toString()}`;
server.routePath('/', (_, response) => {
response.statusCode = 302;
response.setHeader('Location', urlPath);

View file

@ -24,6 +24,7 @@ import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-
import type { HeadersArray } from './types';
export const perMessageDeflate = {
clientNoContextTakeover: true,
zlibDeflateOptions: {
level: 3,
},

View file

@ -243,6 +243,10 @@ export class WKBrowserContext extends BrowserContext {
return this._wkPages().map(wkPage => wkPage._initializedPage).filter(pageOrNull => !!pageOrNull) as Page[];
}
pagesOrErrors() {
return this._wkPages().map(wkPage => wkPage.pageOrError());
}
async newPageDelegate(): Promise<PageDelegate> {
assertBrowserContextIsNotOwned(this);
const { pageProxyId } = await this._browser._browserSession.send('Playwright.createPage', { browserContextId: this._browserContextId });

View file

@ -849,7 +849,7 @@ export class WKPage implements PageDelegate {
this.validateScreenshotDimension(rect.height, omitDeviceScaleFactor);
const result = await this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport', omitDeviceScaleFactor });
const prefix = 'data:image/png;base64,';
let buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
let buffer: Buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
if (format === 'jpeg')
buffer = jpegjs.encode(PNG.sync.read(buffer), quality).data;
return buffer;

View file

@ -91,7 +91,8 @@ function parseLocator(locator: string, testIdAttributeName: string): { selector:
.replace(/newregex\(([^)]+)\)/g, 'r$1')
.replace(/string=/g, '=')
.replace(/regex=/g, '=')
.replace(/,,/g, ',');
.replace(/,,/g, ',')
.replace(/,\)/g, ')');
const preferredQuote = params.map(p => p.quote).filter(quote => '\'"`'.includes(quote))[0] as Quote | undefined;
return { selector: transform(template, params, testIdAttributeName), preferredQuote };
@ -174,6 +175,7 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
.replace(/filter\(,?hasnot2=([^)]+)\)/g, 'internal:has-not=$1')
.replace(/,exact=false/g, '')
.replace(/,exact=true/g, 's')
.replace(/,includehidden=/g, ',include-hidden=')
.replace(/\,/g, '][');
const parts = template.split('.');
@ -233,6 +235,6 @@ export function locatorOrSelectorAsSelector(language: Language, locator: string,
function digestForComparison(language: Language, locator: string) {
locator = locator.replace(/\s/g, '');
if (language === 'javascript')
locator = locator.replace(/\\?["`]/g, '\'');
locator = locator.replace(/\\?["`]/g, '\'').replace(/,{}/g, '');
return locator;
}

View file

@ -25,6 +25,7 @@ let lastConnectionId = 0;
const kConnectionSymbol = Symbol('kConnection');
export const perMessageDeflate = {
serverNoContextTakeover: true,
zlibDeflateOptions: {
level: 3,
},

View file

@ -4294,7 +4294,7 @@ export interface Page {
* takes priority over
* [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout).
*
* @param timeout Maximum time in milliseconds
* @param timeout Maximum time in milliseconds. Pass `0` to disable timeout.
*/
setDefaultTimeout(timeout: number): void;
@ -9193,7 +9193,7 @@ export interface BrowserContext {
* take priority over
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout).
*
* @param timeout Maximum time in milliseconds
* @param timeout Maximum time in milliseconds. Pass `0` to disable timeout.
*/
setDefaultTimeout(timeout: number): void;
@ -12428,7 +12428,7 @@ export interface Locator {
/**
* Captures the aria snapshot of the given element. Read more about [aria snapshots](https://playwright.dev/docs/aria-snapshots) and
* [expect(locator).toMatchAriaSnapshot(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot)
* [expect(locator).toMatchAriaSnapshot(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
* for the corresponding assertion.
*
* **Usage**
@ -18590,6 +18590,19 @@ export interface Clock {
* await page.clock.pauseAt('2020-02-02');
* ```
*
* For best results, install the clock before navigating the page and set it to a time slightly before the intended
* test time. This ensures that all timers run normally during page loading, preventing the page from getting stuck.
* Once the page has fully loaded, you can safely use
* [clock.pauseAt(time)](https://playwright.dev/docs/api/class-clock#clock-pause-at) to pause the clock.
*
* ```js
* // Initialize clock with some time before the test time and let the page load
* // naturally. `Date.now` will progress as the timers fire.
* await page.clock.install({ time: new Date('2024-12-10T08:00:00') });
* await page.goto('http://localhost:3333');
* await page.clock.pauseAt(new Date('2024-12-10T10:00:00'));
* ```
*
* @param time Time to pause at.
*/
pauseAt(time: number|string|Date): Promise<void>;

View file

@ -52,7 +52,7 @@ export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig
export function defineConfig<T>(config: PlaywrightTestConfig<T>): PlaywrightTestConfig<T>;
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>): PlaywrightTestConfig<T, W>;
export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig;
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T>;
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T, W>;
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig<T>[]): PlaywrightTestConfig<T>;
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>, ...configs: PlaywrightTestConfig<T, W>[]): PlaywrightTestConfig<T, W>;
export { expect, devices, Locator } from 'playwright/test';

View file

@ -14,8 +14,10 @@
* limitations under the License.
*/
import type React from 'react';
export declare function beforeMount<HooksConfig>(
callback: (params: { hooksConfig?: HooksConfig; App: () => JSX.Element }) => Promise<void | JSX.Element>
callback: (params: { hooksConfig?: HooksConfig; App: () => React.JSX.Element }) => Promise<void | React.JSX.Element>
): void;
export declare function afterMount<HooksConfig>(
callback: (params: { hooksConfig?: HooksConfig }) => Promise<void>

View file

@ -14,6 +14,8 @@
* limitations under the License.
*/
import type React from 'react';
import type { TestType, Locator } from '@playwright/experimental-ct-core';
export interface MountOptions<HooksConfig> {
@ -22,12 +24,12 @@ export interface MountOptions<HooksConfig> {
export interface MountResult extends Locator {
unmount(): Promise<void>;
update(component: JSX.Element): Promise<void>;
update(component: React.JSX.Element): Promise<void>;
}
export const test: TestType<{
mount<HooksConfig>(
component: JSX.Element,
component: React.JSX.Element,
options?: MountOptions<HooksConfig>
): Promise<MountResult>;
}>;

View file

@ -68,7 +68,7 @@ export async function loadTestFile(file: string, rootDir: string, testErrors?: T
suite.allTests().map(t => files.add(t.location.file));
if (files.size === 1) {
// All tests point to one file.
const mappedFile = files.values().next().value;
const mappedFile = files.values().next().value!;
if (suite.location.file !== mappedFile) {
// The file is different, check for a likely source map case.
if (path.extname(mappedFile) !== path.extname(suite.location.file))

View file

@ -18,19 +18,25 @@
import type { LocatorEx } from './matchers';
import type { ExpectMatcherState } from '../../types/test';
import { kNoElementsFoundError, matcherHint, type MatcherResult } from './matcherHint';
import { colors } from 'playwright-core/lib/utilsBundle';
import { EXPECTED_COLOR } from '../common/expectBundle';
import { callLogText } from '../util';
import { callLogText, sanitizeFilePathBeforeExtension, trimLongString } from '../util';
import { printReceivedStringContainExpectedSubstring } from './expect';
import { currentTestInfo } from '../common/globals';
import type { MatcherReceived } from '@injected/ariaSnapshot';
import { escapeTemplateString } from 'playwright-core/lib/utils';
import { escapeTemplateString, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
import fs from 'fs';
import path from 'path';
type ToMatchAriaSnapshotExpected = {
name?: string;
path?: string;
} | string;
export async function toMatchAriaSnapshot(
this: ExpectMatcherState,
receiver: LocatorEx,
expected: string,
options: { timeout?: number, matchSubstring?: boolean } = {},
expectedParam: ToMatchAriaSnapshotExpected,
options: { timeout?: number } = {},
): Promise<MatcherResult<string | RegExp, string>> {
const matcherName = 'toMatchAriaSnapshot';
@ -39,7 +45,7 @@ export async function toMatchAriaSnapshot(
throw new Error(`toMatchAriaSnapshot() must be called during the test`);
if (testInfo._projectInternal.ignoreSnapshots)
return { pass: !this.isNot, message: () => '', name: 'toMatchAriaSnapshot', expected };
return { pass: !this.isNot, message: () => '', name: 'toMatchAriaSnapshot', expected: '' };
const updateSnapshots = testInfo.config.updateSnapshots;
@ -48,12 +54,25 @@ export async function toMatchAriaSnapshot(
promise: this.promise,
};
if (typeof expected !== 'string') {
throw new Error([
matcherHint(this, receiver, matcherName, receiver, expected, matcherOptions),
`${colors.bold('Matcher error')}: ${EXPECTED_COLOR('expected',)} value must be a string`,
this.utils.printWithType('Expected', expected, this.utils.printExpected)
].join('\n\n'));
let expected: string;
let expectedPath: string | undefined;
if (isString(expectedParam)) {
expected = expectedParam;
} else {
if (expectedParam?.path) {
expectedPath = expectedParam.path;
} else if (expectedParam?.name) {
expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name));
} else {
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;
if (!snapshotNames) {
snapshotNames = { anonymousSnapshotIndex: 0 };
(testInfo as any)[snapshotNamesSymbol] = snapshotNames;
}
const fullTitleWithoutSpec = [...testInfo.titlePath.slice(1), ++snapshotNames.anonymousSnapshotIndex].join(' ');
expectedPath = testInfo.snapshotPath(sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.yml');
}
expected = await fs.promises.readFile(expectedPath, 'utf8').catch(() => '');
}
const generateMissingBaseline = updateSnapshots === 'missing' && !expected;
@ -102,8 +121,24 @@ export async function toMatchAriaSnapshot(
if ((updateSnapshots === 'all') ||
(updateSnapshots === 'changed' && pass === this.isNot) ||
generateMissingBaseline) {
const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\`)`;
return { pass: this.isNot, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
if (expectedPath) {
await fs.promises.mkdir(path.dirname(expectedPath), { recursive: true });
await fs.promises.writeFile(expectedPath, typedReceived.regex, 'utf8');
const relativePath = path.relative(process.cwd(), expectedPath);
if (updateSnapshots === 'missing') {
const message = `A snapshot doesn't exist at ${relativePath}, writing actual.`;
testInfo._hasNonRetriableError = true;
testInfo._failWithError(new Error(message));
} else {
const message = `A snapshot is generated at ${relativePath}.`;
/* eslint-disable no-console */
console.log(message);
}
return { pass: true, message: () => '', name: 'toMatchAriaSnapshot' };
} else {
const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\`)`;
return { pass: false, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
}
}
}
@ -134,3 +169,9 @@ function unshift(snapshot: string): string {
function indent(snapshot: string, indent: string): string {
return snapshot.split('\n').map(line => indent + line).join('\n');
}
const snapshotNamesSymbol = Symbol('snapshotNames');
type SnapshotNames = {
anonymousSnapshotIndex: number;
};

View file

@ -309,7 +309,7 @@ class HtmlBuilder {
let singleTestId: string | undefined;
if (htmlReport.stats.total === 1) {
const testFile: TestFile = data.values().next().value.testFile;
const testFile: TestFile = data.values().next().value!.testFile;
singleTestId = testFile.tests[0].testId;
}
@ -602,17 +602,10 @@ type JsonAttachment = {
};
function stdioAttachment(chunk: Buffer | string, type: 'stdout' | 'stderr'): JsonAttachment {
if (typeof chunk === 'string') {
return {
name: type,
contentType: 'text/plain',
body: chunk
};
}
return {
name: type,
contentType: 'application/octet-stream',
body: chunk
contentType: 'text/plain',
body: typeof chunk === 'string' ? chunk : chunk.toString('utf-8')
};
}

View file

@ -479,7 +479,7 @@ type StdioPayload = {
};
function chunkToPayload(type: 'stdout' | 'stderr', chunk: Buffer | string): StdioPayload {
if (chunk instanceof Buffer)
if (chunk instanceof Uint8Array)
return { type, buffer: chunk.toString('base64') };
return { type, text: chunk };
}

View file

@ -1891,7 +1891,7 @@ type ConditionBody<TestArgs> = (args: TestArgs) => boolean;
* ```
*
*/
export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue> {
export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
/**
* Declares a test.
* - `test(title, body)`
@ -5632,7 +5632,7 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
* Learn more about [fixtures](https://playwright.dev/docs/test-fixtures) and [parametrizing tests](https://playwright.dev/docs/test-parameterize).
* @param fixtures An object containing fixtures and/or options. Learn more about [fixtures format](https://playwright.dev/docs/test-fixtures).
*/
extend<T extends KeyValue, W extends KeyValue = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;
extend<T extends {}, W extends {} = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;
/**
* Returns information about the currently running test. This method can only be called during the test execution,
* otherwise it throws.
@ -5653,19 +5653,18 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
info(): TestInfo;
}
type KeyValue = { [key: string]: any };
export type TestFixture<R, Args extends KeyValue> = (args: Args, use: (r: R) => Promise<void>, testInfo: TestInfo) => any;
export type WorkerFixture<R, Args extends KeyValue> = (args: Args, use: (r: R) => Promise<void>, workerInfo: WorkerInfo) => any;
type TestFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | TestFixture<R, Args>;
type WorkerFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | WorkerFixture<R, Args>;
export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extends KeyValue = {}, PW extends KeyValue = {}> = {
export type TestFixture<R, Args extends {}> = (args: Args, use: (r: R) => Promise<void>, testInfo: TestInfo) => any;
export type WorkerFixture<R, Args extends {}> = (args: Args, use: (r: R) => Promise<void>, workerInfo: WorkerInfo) => any;
type TestFixtureValue<R, Args extends {}> = Exclude<R, Function> | TestFixture<R, Args>;
type WorkerFixtureValue<R, Args extends {}> = Exclude<R, Function> | WorkerFixture<R, Args>;
export type Fixtures<T extends {} = {}, W extends {} = {}, PT extends {} = {}, PW extends {} = {}> = {
[K in keyof PW]?: WorkerFixtureValue<PW[K], W & PW> | [WorkerFixtureValue<PW[K], W & PW>, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean }];
} & {
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }];
} & {
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
[K in Exclude<keyof W, keyof PW | keyof PT>]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
} & {
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
[K in Exclude<keyof T, keyof PW | keyof PT>]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
};
type BrowserName = 'chromium' | 'firefox' | 'webkit';
@ -7487,8 +7486,8 @@ export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig
export function defineConfig<T>(config: PlaywrightTestConfig<T>): PlaywrightTestConfig<T>;
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>): PlaywrightTestConfig<T, W>;
export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig;
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T>;
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T, W>;
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig<T>[]): PlaywrightTestConfig<T>;
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>, ...configs: PlaywrightTestConfig<T, W>[]): PlaywrightTestConfig<T, W>;
type MergedT<List> = List extends [TestType<infer T, any>, ...(infer Rest)] ? T & MergedT<Rest> : {};
type MergedW<List> = List extends [TestType<any, infer W>, ...(infer Rest)] ? W & MergedW<Rest> : {};
@ -8431,6 +8430,37 @@ interface LocatorAssertions {
timeout?: number;
}): Promise<void>;
/**
* Asserts that the target element matches the given [accessibility snapshot](https://playwright.dev/docs/aria-snapshots).
*
* **Usage**
*
* ```js
* await expect(page.locator('body')).toMatchAriaSnapshot();
* await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
* await expect(page.locator('body')).toMatchAriaSnapshot({ path: '/path/to/snapshot.yml' });
* ```
*
* @param options
*/
toMatchAriaSnapshot(options?: {
/**
* Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not
* specified.
*/
name?: string;
/**
* Path to the YAML snapshot file.
*/
path?: string;
/**
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
*/
timeout?: number;
}): Promise<void>;
/**
* Asserts that the target element matches the given [accessibility snapshot](https://playwright.dev/docs/aria-snapshots).
*

View file

@ -691,6 +691,7 @@ export interface DebugControllerChannel extends DebugControllerEventTarget, Chan
export type DebugControllerInspectRequestedEvent = {
selector: string,
locator: string,
ariaSnapshot: string,
};
export type DebugControllerSetModeRequestedEvent = {
mode: string,

View file

@ -807,6 +807,7 @@ DebugController:
parameters:
selector: string
locator: string
ariaSnapshot: string
setModeRequested:
parameters:

View file

@ -3,11 +3,6 @@
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && tsc",
"preview": "vite preview"
},
"dependencies": {
"yaml": "^2.6.0"
}

View file

@ -2,11 +2,5 @@
"name": "trace-viewer",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && tsc",
"build-sw": "vite --config vite.sw.config.ts build && tsc",
"preview": "vite preview"
}
"type": "module"
}

View file

@ -37,7 +37,7 @@ export class LRUCache<K, V> {
const result = compute();
while (this._map.size && this._size + result.size > this._maxSize) {
const [firstKey, firstValue] = this._map.entries().next().value;
const [firstKey, firstValue] = this._map.entries().next().value!;
this._size -= firstValue.size;
this._map.delete(firstKey);
}

View file

@ -43,10 +43,8 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, client:
const clientId = client?.id ?? '';
let data = clientIdToTraceUrls.get(clientId);
if (!data) {
let traceViewerServerBaseUrl = new URL('../', client?.url ?? self.registration.scope);
if (traceViewerServerBaseUrl.searchParams.has('server'))
traceViewerServerBaseUrl = new URL(traceViewerServerBaseUrl.searchParams.get('server')!, traceViewerServerBaseUrl);
const clientURL = new URL(client?.url ?? self.registration.scope);
const traceViewerServerBaseUrl = new URL(clientURL.searchParams.get('server') ?? '../', clientURL);
data = { limit, traceUrls: new Set(), traceViewerServer: new TraceViewerServer(traceViewerServerBaseUrl) };
clientIdToTraceUrls.set(clientId, data);
}

View file

@ -152,7 +152,7 @@ export class SnapshotRenderer {
const html = prefix + [
// Hide the document in order to prevent flickering. We will unhide once script has processed shadow.
'<style>*,*::before,*::after { visibility: hidden }</style>',
`<script>${snapshotScript(this._callId, this.snapshotName)}</script>`
`<script>${snapshotScript(this.viewport(), this._callId, this.snapshotName)}</script>`
].join('') + result.join('');
return { value: html, size: html.length };
});
@ -236,12 +236,41 @@ function snapshotNodes(snapshot: FrameSnapshot): NodeSnapshot[] {
return (snapshot as any)._nodes;
}
function snapshotScript(...targetIds: (string | undefined)[]) {
function applyPlaywrightAttributes(unwrapPopoutUrl: (url: string) => string, ...targetIds: (string | undefined)[]) {
type ViewportSize = { width: number, height: number };
type BoundingRect = { left: number, top: number, right: number, bottom: number };
type FrameBoundingRectsInfo = {
viewport: ViewportSize;
frames: WeakMap<Element, {
boundingRect: BoundingRect;
scrollLeft: number;
scrollTop: number;
}>;
};
declare global {
interface Window {
__playwright_frame_bounding_rects__: FrameBoundingRectsInfo;
}
}
function snapshotScript(viewport: ViewportSize, ...targetIds: (string | undefined)[]) {
function applyPlaywrightAttributes(unwrapPopoutUrl: (url: string) => string, viewport: ViewportSize, ...targetIds: (string | undefined)[]) {
const searchParams = new URLSearchParams(location.search);
const shouldPopulateCanvasFromScreenshot = searchParams.has('shouldPopulateCanvasFromScreenshot');
const isUnderTest = searchParams.has('isUnderTest');
// info to recursively compute canvas position relative to the top snapshot frame.
// Before rendering each iframe, its parent extracts the '__playwright_canvas_render_info__' attribute
// value and keeps in this variable. It can then remove the attribute and render the element,
// which will eventually trigger the same process inside the iframe recursively.
// When there's a canvas to render, we iterate over its ancestor frames to compute
// its position relative to the top snapshot frame.
const frameBoundingRectsInfo = {
viewport,
frames: new WeakMap(),
};
window['__playwright_frame_bounding_rects__'] = frameBoundingRectsInfo;
const kPointerWarningTitle = 'Recorded click position in absolute coordinates did not' +
' match the center of the clicked element. This is likely due to a difference between' +
' the test runner and the trace viewer operating systems.';
@ -251,6 +280,10 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
const targetElements: Element[] = [];
const canvasElements: HTMLCanvasElement[] = [];
let topSnapshotWindow: Window = window;
while (topSnapshotWindow !== topSnapshotWindow.parent && !topSnapshotWindow.location.pathname.match(/\/page@[a-z0-9]+$/))
topSnapshotWindow = topSnapshotWindow.parent;
const visit = (root: Document | ShadowRoot) => {
// Collect all scrolled elements for later use.
for (const e of root.querySelectorAll(`[__playwright_scroll_top_]`))
@ -290,6 +323,11 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
}
for (const iframe of root.querySelectorAll('iframe, frame')) {
const boundingRectJson = iframe.getAttribute('__playwright_bounding_rect__');
iframe.removeAttribute('__playwright_bounding_rect__');
const boundingRect = boundingRectJson ? JSON.parse(boundingRectJson) : undefined;
if (boundingRect)
frameBoundingRectsInfo.frames.set(iframe, { boundingRect, scrollLeft: 0, scrollTop: 0 });
const src = iframe.getAttribute('__playwright_src__');
if (!src) {
iframe.setAttribute('src', 'data:text/html,<body style="background: #ddd"></body>');
@ -341,16 +379,20 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
for (const element of scrollTops) {
element.scrollTop = +element.getAttribute('__playwright_scroll_top_')!;
element.removeAttribute('__playwright_scroll_top_');
if (frameBoundingRectsInfo.frames.has(element))
frameBoundingRectsInfo.frames.get(element)!.scrollTop = element.scrollTop;
}
for (const element of scrollLefts) {
element.scrollLeft = +element.getAttribute('__playwright_scroll_left_')!;
element.removeAttribute('__playwright_scroll_left_');
if (frameBoundingRectsInfo.frames.has(element))
frameBoundingRectsInfo.frames.get(element)!.scrollLeft = element.scrollTop;
}
document.styleSheets[0].disabled = true;
const search = new URL(window.location.href).searchParams;
const isTopFrame = window.location.pathname.match(/\/page@[a-z0-9]+$/);
const isTopFrame = window === topSnapshotWindow;
if (search.get('pointX') && search.get('pointY')) {
const pointX = +search.get('pointX')!;
@ -421,16 +463,6 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
context.fillRect(0, 0, canvas.width, canvas.height);
}
if (!isTopFrame) {
for (const canvas of canvasElements) {
const context = canvas.getContext('2d')!;
drawCheckerboard(context, canvas);
canvas.title = `Playwright displays canvas contents on a best-effort basis. It doesn't support canvas elements inside an iframe yet. If this impacts your workflow, please open an issue so we can prioritize.`;
}
return;
}
const img = new Image();
img.onload = () => {
for (const canvas of canvasElements) {
@ -448,6 +480,31 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
continue;
}
let currWindow: Window = window;
while (currWindow !== topSnapshotWindow) {
const iframe = currWindow.frameElement!;
currWindow = currWindow.parent;
const iframeInfo = currWindow['__playwright_frame_bounding_rects__']?.frames.get(iframe);
if (!iframeInfo?.boundingRect)
break;
const leftOffset = iframeInfo.boundingRect.left - iframeInfo.scrollLeft;
const topOffset = iframeInfo.boundingRect.top - iframeInfo.scrollTop;
boundingRect.left += leftOffset;
boundingRect.top += topOffset;
boundingRect.right += leftOffset;
boundingRect.bottom += topOffset;
}
const { width, height } = topSnapshotWindow['__playwright_frame_bounding_rects__'].viewport;
boundingRect.left = boundingRect.left / width;
boundingRect.top = boundingRect.top / height;
boundingRect.right = boundingRect.right / width;
boundingRect.bottom = boundingRect.bottom / height;
const partiallyUncaptured = boundingRect.right > 1 || boundingRect.bottom > 1;
const fullyUncaptured = boundingRect.left > 1 || boundingRect.top > 1;
if (fullyUncaptured) {
@ -490,7 +547,7 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
window.addEventListener('DOMContentLoaded', onDOMContentLoaded);
}
return `\n(${applyPlaywrightAttributes.toString()})(${unwrapPopoutUrl.toString()}${targetIds.map(id => `, "${id}"`).join('')})`;
return `\n(${applyPlaywrightAttributes.toString()})(${unwrapPopoutUrl.toString()}, ${JSON.stringify(viewport)}${targetIds.map(id => `, "${id}"`).join('')})`;
}

View file

@ -120,7 +120,7 @@ const ResponseTab: React.FunctionComponent<{
const BodyTab: React.FunctionComponent<{
resource: ResourceSnapshot;
}> = ({ resource }) => {
const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, mimeType?: string, font?: BinaryData } | null>(null);
const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, mimeType?: string, font?: BufferSource } | null>(null);
React.useEffect(() => {
const readResources = async () => {
@ -158,7 +158,7 @@ const BodyTab: React.FunctionComponent<{
};
const FontPreview: React.FunctionComponent<{
font: BinaryData;
font: BufferSource;
}> = ({ font }) => {
const [isError, setIsError] = React.useState(false);

View file

@ -351,7 +351,8 @@ export function extendSnapshot(snapshot: Snapshot, shouldPopulateCanvasFromScree
const popoutParams = new URLSearchParams();
popoutParams.set('r', snapshotUrl);
popoutParams.set('server', serverParam ?? '');
if (serverParam)
popoutParams.set('server', serverParam);
popoutParams.set('trace', context(snapshot.action).traceUrl);
if (snapshot.point) {
popoutParams.set('pointX', String(snapshot.point.x));

View file

@ -47,9 +47,7 @@ const xtermDataSource: XtermDataSource = {
};
const searchParams = new URLSearchParams(window.location.search);
let testServerBaseUrl = new URL('../', window.location.href);
if (testServerBaseUrl.searchParams.has('server'))
testServerBaseUrl = new URL(testServerBaseUrl.searchParams.get('server')!, testServerBaseUrl);
const testServerBaseUrl = new URL(searchParams.get('server') ?? '../', window.location.href);
const wsURL = new URL(searchParams.get('ws')!, testServerBaseUrl);
wsURL.protocol = (wsURL.protocol === 'https:' ? 'wss:' : 'ws:');
const queryParams = {

View file

@ -180,7 +180,7 @@ export function TreeView<T extends TreeItem>({
const itemData = treeItems.get(child as T);
return itemData && <TreeItemHeader
key={child.id}
item={child}
item={child as T}
treeItems={treeItems}
selectedItem={selectedItem}
onSelected={onSelected}

View file

@ -31,6 +31,7 @@ test('render an empty component', async ({ mount, page }) => {
const testWithServer = test.extend(serverFixtures);
testWithServer(
'components routing should go through context',
// @ts-ignore "serverFixtures" are imported from the impl without any types
async ({ mount, context, server }) => {
server.setRoute('/hello', (req: any, res: any) => {
res.write('served via server');

View file

@ -10,8 +10,8 @@
},
"dependencies": {
"core-js": "^3.8.3",
"vue": "3.2.36",
"vue-router": "^4.1.5"
"vue": "^3.2.36",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@babel/core": "^7.23.2",
@ -20,7 +20,7 @@
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/tsconfig": "^0.1.3",
"@vue/tsconfig": "^0.7.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
},

View file

@ -1,5 +1,5 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["src/**/*", "src/**/*.vue"],
"exclude": ["src/**/*.spec.*/*"],
"compilerOptions": {

View file

@ -1,5 +1,5 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["playwright.config.*"],
"compilerOptions": {
"composite": true,

View file

@ -1,9 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["vite.config.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"],
"ignoreDeprecations": "5.0"
}
}

View file

@ -99,7 +99,7 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
isWebView2: [false, { scope: 'worker' }],
isHeadlessShell: [async ({ browserName, channel, headless }, use) => {
await use(browserName === 'chromium' && (channel === 'chromium-headless-shell' || (!channel && headless)));
await use(browserName === 'chromium' && (channel === 'chromium-headless-shell' || channel === 'chromium-tip-of-tree-headless-shell' || (!channel && headless)));
}, { scope: 'worker' }],
contextFactory: async ({ _contextFactory }: any, run) => {

View file

@ -92,7 +92,7 @@ export class RemoteServer implements PlaywrightServer {
handleSIGINT: true,
handleSIGTERM: true,
handleSIGHUP: true,
executablePath: browserOptions.channel ? undefined : browserOptions.executablePath || browserType.executablePath(),
executablePath: browserOptions.channel ? undefined : browserOptions.executablePath,
logger: undefined,
};
const options = {

View file

@ -14,6 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './npmTest';
import { chromium } from '@playwright/test';
import path from 'path';
test.use({ isolateBrowsers: true });
@ -95,3 +96,46 @@ test('install playwright-chromium should work', async ({ exec, installedSoftware
await exec('npx playwright install chromium');
await exec('node sanity.js playwright-chromium chromium');
});
test('should print error if recording video without ffmpeg', async ({ exec, writeFiles }) => {
await exec('npm i playwright');
await writeFiles({
'launch.js': `
const playwright = require('playwright');
(async () => {
const browser = await playwright.chromium.launch({ executablePath: ${JSON.stringify(chromium.executablePath())} });
try {
const context = await browser.newContext({ recordVideo: { dir: 'videos' } });
const page = await context.newPage();
} finally {
await browser.close();
}
})().catch(e => {
console.error(e);
process.exit(1);
});
`,
'launchPersistentContext.js': `
const playwright = require('playwright');
process.on('unhandledRejection', (e) => console.error('unhandledRejection', e));
(async () => {
const context = await playwright.chromium.launchPersistentContext('', { executablePath: ${JSON.stringify(chromium.executablePath())}, recordVideo: { dir: 'videos' } });
})().catch(e => {
console.error(e);
process.exit(1);
});
`,
});
await test.step('BrowserType.launch', async () => {
const result = await exec('node', 'launch.js', { expectToExitWithError: true });
expect(result).toContain(`browserContext.newPage: Executable doesn't exist at`);
});
await test.step('BrowserType.launchPersistentContext', async () => {
const result = await exec('node', 'launchPersistentContext.js', { expectToExitWithError: true });
expect(result).not.toContain('unhandledRejection');
expect(result).toContain(`browserType.launchPersistentContext: Executable doesn't exist at`);
});
});

View file

@ -84,9 +84,11 @@ test('should pick element', async ({ backend, connectedBrowser }) => {
expect(events).toEqual([
{
ariaSnapshot: '- button "Submit"',
selector: 'internal:role=button[name=\"Submit\"i]',
locator: 'getByRole(\'button\', { name: \'Submit\' })',
}, {
ariaSnapshot: '- button "Submit"',
selector: 'internal:role=button[name=\"Submit\"i]',
locator: 'getByRole(\'button\', { name: \'Submit\' })',
},

View file

@ -19,7 +19,7 @@ import { PNG } from 'playwright-core/lib/utilsBundle';
import { expect, playwrightTest as it } from '../config/browserTest';
it.use({ headless: false });
it.skip(({ channel }) => channel === 'chromium-headless-shell', 'shell is never headed');
it.skip(({ channel }) => channel === 'chromium-headless-shell' || channel === 'chromium-tip-of-tree-headless-shell', 'shell is never headed');
it('should have default url when launching browser @smoke', async ({ launchPersistent }) => {
const { context } = await launchPersistent();

View file

@ -926,4 +926,34 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`)
const predicate = (msg: ConsoleMessage) => msg.type() === 'error' && /Content[\- ]Security[\- ]Policy/i.test(msg.text());
await expect(page.waitForEvent('console', { predicate, timeout: 1000 })).rejects.toThrow();
});
test('should clear when recording is disabled', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33802' } }, async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`
<button id="foo" onclick="console.log('click')">Foo</button>
<button id="bar" onclick="console.log('click')">Bar</button>
`);
await recorder.hoverOverElement('#foo');
let [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'click'),
recorder.trustedClick(),
]);
expect(sources.get('JavaScript').text).toContain(`getByRole('button', { name: 'Foo' }).click()`);
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.recorderPage.getByRole('button', { name: 'Clear' }).click();
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.hoverOverElement('#bar');
[sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'click'),
recorder.trustedClick(),
]);
expect(sources.get('JavaScript').text).toContain(`getByRole('button', { name: 'Bar' }).click()`);
expect(sources.get('JavaScript').text).not.toContain(`getByRole('button', { name: 'Foo' })`);
});
});

View file

@ -472,7 +472,7 @@ await page.GetByTestId("testid").ClickAsync();`);
await recorder.setContentAndWait(`<input placeholder="Country"></input>`);
const locator = await recorder.hoverOverElement('input');
expect(locator).toBe(`getByPlaceholder('Country')`);
expect(locator).toBe(`getByRole('textbox', { name: 'Country' })`);
const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'click'),
@ -480,19 +480,19 @@ await page.GetByTestId("testid").ClickAsync();`);
]);
expect.soft(sources.get('JavaScript')!.text).toContain(`
await page.getByPlaceholder('Country').click();`);
await page.getByRole('textbox', { name: 'Country' }).click();`);
expect.soft(sources.get('Python')!.text).toContain(`
page.get_by_placeholder("Country").click()`);
page.get_by_role("textbox", name="Country").click()`);
expect.soft(sources.get('Python Async')!.text).toContain(`
await page.get_by_placeholder("Country").click()`);
await page.get_by_role("textbox", name="Country").click()`);
expect.soft(sources.get('Java')!.text).toContain(`
page.getByPlaceholder("Country").click()`);
page.getByRole(AriaRole.TEXTBOX, new Page.GetByRoleOptions().setName("Country")).click()`);
expect.soft(sources.get('C#')!.text).toContain(`
await page.GetByPlaceholder("Country").ClickAsync();`);
await page.GetByRole(AriaRole.Textbox, new() { Name = "Country" }).ClickAsync();`);
});
test('should generate getByAltText', async ({ openRecorder }) => {
@ -530,7 +530,7 @@ await page.GetByAltText("Country").ClickAsync();`);
await recorder.setContentAndWait(`<label for=target>Country</label><input id=target>`);
const locator = await recorder.hoverOverElement('input');
expect(locator).toBe(`getByLabel('Country')`);
expect(locator).toBe(`getByRole('textbox', { name: 'Country' })`);
const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'click'),
@ -538,19 +538,19 @@ await page.GetByAltText("Country").ClickAsync();`);
]);
expect.soft(sources.get('JavaScript')!.text).toContain(`
await page.getByLabel('Country').click();`);
await page.getByRole('textbox', { name: 'Country' }).click();`);
expect.soft(sources.get('Python')!.text).toContain(`
page.get_by_label("Country").click()`);
page.get_by_role("textbox", name="Country").click()`);
expect.soft(sources.get('Python Async')!.text).toContain(`
await page.get_by_label("Country").click()`);
await page.get_by_role("textbox", name="Country").click()`);
expect.soft(sources.get('Java')!.text).toContain(`
page.getByLabel("Country").click()`);
page.getByRole(AriaRole.TEXTBOX, new Page.GetByRoleOptions().setName("Country")).click()`);
expect.soft(sources.get('C#')!.text).toContain(`
await page.GetByLabel("Country").ClickAsync();`);
await page.GetByRole(AriaRole.Textbox, new() { Name = "Country" }).ClickAsync();`);
});
test('should generate getByLabel without regex', async ({ openRecorder }) => {
@ -559,7 +559,7 @@ await page.GetByLabel("Country").ClickAsync();`);
await recorder.setContentAndWait(`<label for=target>Coun"try</label><input id=target>`);
const locator = await recorder.hoverOverElement('input');
expect(locator).toBe(`getByLabel('Coun"try')`);
expect(locator).toBe(`getByRole('textbox', { name: 'Coun"try' })`);
const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'click'),
@ -567,19 +567,19 @@ await page.GetByLabel("Country").ClickAsync();`);
]);
expect.soft(sources.get('JavaScript')!.text).toContain(`
await page.getByLabel('Coun\"try').click();`);
await page.getByRole('textbox', { name: 'Coun\"try' }).click();`);
expect.soft(sources.get('Python')!.text).toContain(`
page.get_by_label("Coun\\"try").click()`);
page.get_by_role("textbox", name="Coun\\"try").click()`);
expect.soft(sources.get('Python Async')!.text).toContain(`
await page.get_by_label("Coun\\"try").click()`);
await page.get_by_role("textbox", name="Coun\\"try").click()`);
expect.soft(sources.get('Java')!.text).toContain(`
page.getByLabel("Coun\\"try").click()`);
page.getByRole(AriaRole.TEXTBOX, new Page.GetByRoleOptions().setName(\"Coun\\\"try\")).click();`);
expect.soft(sources.get('C#')!.text).toContain(`
await page.GetByLabel("Coun\\"try").ClickAsync();`);
await page.GetByRole(AriaRole.Textbox, new() { Name = \"Coun\\\"try\" }).ClickAsync();`);
});
test('should consume pointer events', async ({ openRecorder }) => {

View file

@ -45,6 +45,7 @@ it('should throw a friendly error if its headed and there is no xserver on linux
it.skip(platform !== 'linux');
it.skip(mode.startsWith('service'));
it.skip(channel === 'chromium-headless-shell', 'shell is never headed');
it.skip(channel === 'chromium-tip-of-tree-headless-shell', 'shell is never headed');
const error: Error = await browserType.launch({
headless: false,

View file

@ -196,6 +196,12 @@ it('reverse engineer getByRole', async ({ page }) => {
java: `getByRole(AriaRole.BUTTON)`,
csharp: `GetByRole(AriaRole.Button)`,
});
expect.soft(generate(page.getByRole('heading', {}))).toEqual({
javascript: "getByRole('heading')",
python: 'get_by_role("heading")',
java: 'getByRole(AriaRole.HEADING)',
csharp: 'GetByRole(AriaRole.Heading)'
});
expect.soft(generate(page.getByRole('button', { name: 'Hello' }))).toEqual({
javascript: `getByRole('button', { name: 'Hello' })`,
python: `get_by_role("button", name="Hello")`,
@ -559,6 +565,12 @@ it('parseLocator css', async () => {
expect.soft(parseLocator('csharp', `Locator("css=.foo")`, '')).toBe(`css=.foo`);
});
it('parseLocator options', async () => {
expect.soft(parseLocator('javascript', `getByRole('heading', {})`, '')).toBe(`internal:role=heading`);
expect.soft(parseLocator('javascript', `getByRole('checkbox', { checked:false, includeHidden: true })`, '')).toBe(`internal:role=checkbox[checked=false][include-hidden=true]`);
});
it('parse locators strictly', () => {
const selector = 'div >> internal:has-text=\"Goodbye world\"i >> span';

View file

@ -58,7 +58,7 @@ it.describe('selector generator', () => {
it('should not escape spaces inside named attr selectors', async ({ page }) => {
await page.setContent(`<input placeholder="Foo b ar"/>`);
expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"Foo b ar\"i]');
expect(await generate(page, 'input')).toBe('internal:role=textbox[name=\"Foo b ar\"i]');
});
it('should generate text for <input type=button>', async ({ page }) => {
@ -91,7 +91,7 @@ it.describe('selector generator', () => {
it('should try to improve label text by shortening', async ({ page }) => {
await page.setContent(`<label>Longest verbose description of the item<input></label>`);
expect(await generate(page, 'input')).toBe('internal:label="Longest verbose description"i');
expect(await generate(page, 'input')).toBe('internal:role=textbox[name=\"Longest verbose description\"i]');
});
it('should not improve guid text', async ({ page }) => {
@ -309,14 +309,14 @@ it.describe('selector generator', () => {
expect(await generate(page, 'input[mark="1"]')).toBe('internal:role=textbox >> nth=1');
});
it.describe('should prioritise attributes correctly', () => {
it.describe('should prioritize attributes correctly', () => {
it('role', async ({ page }) => {
await page.setContent(`<input name="foobar" type="text"/>`);
expect(await generate(page, 'input')).toBe('internal:role=textbox');
});
it('placeholder', async ({ page }) => {
await page.setContent(`<input placeholder="foobar" type="text"/>`);
expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"foobar\"i]');
expect(await generate(page, 'input')).toBe('internal:role=textbox[name=\"foobar\"i]');
});
it('name', async ({ page }) => {
await page.setContent(`
@ -437,7 +437,7 @@ it.describe('selector generator', () => {
it('should accept valid aria-label for candidate consideration', async ({ page }) => {
await page.setContent(`<button aria-label="ariaLabel" id="buttonId"></button>`);
expect(await generate(page, 'button')).toBe('internal:label="ariaLabel"i');
expect(await generate(page, 'button')).toBe('internal:role=button[name=\"ariaLabel\"i]');
});
it('should ignore empty role for candidate consideration', async ({ page }) => {
@ -469,15 +469,15 @@ it.describe('selector generator', () => {
<label for=target5>Target5</label><input id=target5 type=hidden>
<label for=target6>Target6</label><div id=target6>text</div>
`);
expect(await generate(page, '#target1')).toBe('internal:label="Target1"i');
expect(await generate(page, '#target2')).toBe('internal:label="Target2"i');
expect(await generate(page, '#target3')).toBe('internal:label="Target3"i');
expect(await generate(page, '#target4')).toBe('internal:label="Target4"i');
expect(await generate(page, '#target5')).toBe('#target5');
expect(await generate(page, '#target6')).toBe('internal:text="text"i');
expect.soft(await generate(page, '#target1')).toBe('internal:role=textbox[name=\"Target1\"i]');
expect.soft(await generate(page, '#target2')).toBe('internal:role=button[name=\"Target2\"i]');
expect.soft(await generate(page, '#target3')).toBe('internal:label=\"Target3\"i');
expect.soft(await generate(page, '#target4')).toBe('internal:label=\"Target4\"i');
expect.soft(await generate(page, '#target5')).toBe('#target5');
expect.soft(await generate(page, '#target6')).toBe('internal:text="text"i');
await page.setContent(`<label for=target>Coun"try</label><input id=target>`);
expect(await generate(page, 'input')).toBe('internal:label="Coun\\\"try"i');
expect(await generate(page, 'input')).toBe('internal:role=textbox[name=\"Coun\\\"try\"i]');
});
it('should prefer role other input[type]', async ({ page }) => {
@ -514,7 +514,7 @@ it.describe('selector generator', () => {
<input placeholder="Text"></input>
<input placeholder="Text and more"></input>
`);
expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"Text\"s]');
expect(await generate(page, 'input')).toBe('internal:role=textbox[name=\"Text\"s]');
});
it('should generate exact role when necessary', async ({ page }) => {
@ -530,7 +530,7 @@ it.describe('selector generator', () => {
<label>Text <input></input></label>
<label>Text and more <input></input></label>
`);
expect(await generate(page, 'input')).toBe('internal:label=\"Text\"s');
expect(await generate(page, 'input')).toBe('internal:role=textbox[name=\"Text\"s]');
});
it('should generate relative selector', async ({ page }) => {
@ -596,4 +596,23 @@ it.describe('selector generator', () => {
`span >> nth=1`,
]);
});
it('should prefer role with hasText to css with hasText', async ({ page }) => {
await page.setContent(`
<ul>
<li>
<input aria-label="Toggle Todo" type="checkbox">
buy flowers
</li>
<li>
<input aria-label="Toggle Todo" type="checkbox">
sell milk
</li>
</ul>
`);
expect(await generateMultiple(page, 'input')).toEqual([
`internal:role=listitem >> internal:has-text=\"buy flowers\"i >> internal:label=\"Toggle Todo\"i`,
`internal:label=\"Toggle Todo\"i >> nth=0`,
]);
});
});

View file

@ -268,10 +268,12 @@ it.describe('snapshots', () => {
});
});
function distillSnapshot(snapshot, distillTarget = true) {
function distillSnapshot(snapshot, options: { distillTarget: boolean, distillBoundingRect: boolean } = { distillTarget: true, distillBoundingRect: true }) {
let { html } = snapshot.render();
if (distillTarget)
if (options.distillTarget)
html = html.replace(/\s__playwright_target__="[^"]+"/g, '');
if (options.distillBoundingRect)
html = html.replace(/\s__playwright_bounding_rect__="[^"]+"/g, '');
return html
.replace(/<style>\*,\*::before,\*::after { visibility: hidden }<\/style>/, '')
.replace(/<script>[.\s\S]+<\/script>/, '')

View file

@ -1539,12 +1539,16 @@ test('canvas clipping in iframe', async ({ runAndTrace, page, server }) => {
await page.setContent(`
<iframe src="${server.PREFIX}/screenshots/canvas.html#canvas-on-edge"></iframe>
`);
await page.locator('iframe').contentFrame().locator('canvas').scrollIntoViewIfNeeded();
await rafraf(page, 5);
});
const msg = await traceViewer.page.waitForEvent('console', { predicate: msg => msg.text().startsWith('canvas drawn:') });
expect(msg.text()).toEqual('canvas drawn: [1,1,11,20]');
const snapshot = await traceViewer.snapshotFrame('page.evaluate');
const canvas = snapshot.locator('iframe').contentFrame().locator('canvas');
await expect(canvas).toHaveAttribute('title', `Playwright displays canvas contents on a best-effort basis. It doesn't support canvas elements inside an iframe yet. If this impacts your workflow, please open an issue so we can prioritize.`);
await expect(canvas).toHaveAttribute('title', 'Canvas contents are displayed on a best-effort basis based on viewport screenshots taken during test execution.');
});
test('should show only one pointer with multilevel iframes', async ({ page, runAndTrace, server, browserName }) => {

View file

@ -421,6 +421,18 @@ it('should treat input value as text in templates', async ({ page }) => {
`);
});
it('should not use on as checkbox value', async ({ page }) => {
await page.setContent(`
<input type='checkbox'>
<input type='radio'>
`);
await checkAndMatchSnapshot(page.locator('body'), `
- checkbox
- radio
`);
});
it('should respect aria-owns', async ({ page }) => {
await page.setContent(`
<a href='about:blank' aria-owns='input p'>

View file

@ -183,6 +183,17 @@ it('should work across nodes', async ({ page }) => {
expect(await page.$$eval(`text=/world/`, els => els.length)).toBe(1);
});
it('text-is() should ignore comments', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33976' }
}, async ({ page }) => {
await page.setContent(`<div id=me>hel<!-- comment -->lo
<!-- comment -->
world</div>`);
expect(await page.$eval(`:text-is("hello world")`, e => e.id)).toBe('me');
expect(await page.locator('div', { hasText: 'hello world' }).getAttribute('id')).toBe('me');
expect(await page.getByText('hello world', { exact: true }).getAttribute('id')).toBe('me');
});
it('should work with text nodes in quoted mode', async ({ page }) => {
await page.setContent(`<div id=target1>Hello<span id=target2>wo rld </span> Hi again </div>`);
expect(await page.$eval(`text="Hello"`, e => e.id)).toBe('target1');

View file

@ -0,0 +1,172 @@
/**
* Copyright Microsoft Corporation. All rights reserved.
*
* 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 { test, expect } from './playwright-test-fixtures';
import fs from 'fs';
import path from 'path';
test.describe.configure({ mode: 'parallel' });
test('should match snapshot with name', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
};
`,
'__snapshots__/a.spec.ts/test.yml': `
- heading "hello world"
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello world</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.yml' });
});
`
});
expect(result.exitCode).toBe(0);
});
test('should match snapshot with path', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'test.yml': `
- heading "hello world"
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
import path from 'path';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello world</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot({ path: path.resolve(__dirname, 'test.yml') });
});
`
});
expect(result.exitCode).toBe(0);
});
test('should generate multiple missing', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
};
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello world</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test-1.yml' });
await page.setContent(\`<h1>hello world 2</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test-2.yml' });
});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-1.yml, writing actual`);
expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-2.yml, writing actual`);
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-1.yml'), 'utf8');
expect(snapshot1).toBe('- heading "hello world" [level=1]');
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-2.yml'), 'utf8');
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
});
test('should rebaseline all', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
};
`,
'__snapshots__/a.spec.ts/test-1.yml': `
- heading "foo"
`,
'__snapshots__/a.spec.ts/test-2.yml': `
- heading "bar"
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello world</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test-1.yml' });
await page.setContent(\`<h1>hello world 2</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test-2.yml' });
});
`
}, { 'update-snapshots': 'all' });
expect(result.exitCode).toBe(0);
expect(result.output).toContain(`A snapshot is generated at __snapshots__${path.sep}a.spec.ts${path.sep}test-1.yml`);
expect(result.output).toContain(`A snapshot is generated at __snapshots__${path.sep}a.spec.ts${path.sep}test-2.yml`);
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-1.yml'), 'utf8');
expect(snapshot1).toBe('- heading "hello world" [level=1]');
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-2.yml'), 'utf8');
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
});
test('should not rebaseline matching', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
};
`,
'__snapshots__/a.spec.ts/test.yml': `
- heading "hello world"
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello world</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'test.yml' });
});
`
}, { 'update-snapshots': 'changed' });
expect(result.exitCode).toBe(0);
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test.yml'), 'utf8');
expect(snapshot1.trim()).toBe('- heading "hello world"');
});
test('should generate snapshot name', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
snapshotPathTemplate: '__snapshots__/{testFilePath}/{arg}{ext}',
};
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test name', async ({ page }) => {
await page.setContent(\`<h1>hello world</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot();
await page.setContent(\`<h1>hello world 2</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot();
});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-name-1.yml, writing actual`);
expect(result.output).toContain(`A snapshot doesn't exist at __snapshots__${path.sep}a.spec.ts${path.sep}test-name-2.yml, writing actual`);
const snapshot1 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-name-1.yml'), 'utf8');
expect(snapshot1).toBe('- heading "hello world" [level=1]');
const snapshot2 = await fs.promises.readFile(testInfo.outputPath('__snapshots__/a.spec.ts/test-name-2.yml'), 'utf8');
expect(snapshot2).toBe('- heading "hello world 2" [level=1]');
});

View file

@ -2562,6 +2562,24 @@ for (const useIntermediateMergeReport of [true, false] as const) {
- button "tests/b/test.spec.ts"
`);
});
test('execSync doesnt produce a second stdout attachment', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33886' } }, async ({ runInlineTest, showReport, page }) => {
await runInlineTest({
'a.test.js': `
const { test, expect } = require('@playwright/test');
const { execSync } = require('node:child_process');
test('my test', async ({}) => {
console.log('foo');
execSync('echo bar', { stdio: 'inherit' });
console.log('baz');
});
`,
}, { reporter: 'dot,html' });
await showReport();
await page.getByText('my test').click();
await expect(page.locator('.tree-item', { hasText: 'stdout' })).toHaveCount(1);
});
});
}

View file

@ -115,6 +115,71 @@ test('should check types of fixtures', async ({ runTSC }) => {
await use(x);
},
});
base.extend({
page: async ({ page }) => {
type IsPage = (typeof page) extends Page ? true : never;
const isPage: IsPage = true;
},
});
base.extend<{ myFixture: (arg: number) => void }>({
page: async ({ page }) => {
type IsPage = (typeof page) extends Page ? true : never;
const isPage: IsPage = true;
},
});
base.extend({
// @ts-expect-error
myFixture: async ({ page }) => {
type IsPage = (typeof page) extends Page ? true : never;
const isPage: IsPage = true;
}
});
base.extend<{ myFixture: (arg: number) => void }>({
myFixture: async ({ page }) => {
type IsPage = (typeof page) extends Page ? true : never;
const isPage: IsPage = true;
}
});
base.extend({
page: async ({ page }) => {
type IsPage = (typeof page) extends Page ? true : never;
const isPage: IsPage = true;
},
// @ts-expect-error
myFixture: async ({ page }) => {
type IsPage = (typeof page) extends Page ? true : never;
const isPage: IsPage = true;
}
});
base.extend<{ myFixture: (arg: number) => void }>({
page: async ({ page }) => {
type IsPage = (typeof page) extends Page ? true : never;
const isPage: IsPage = true;
},
myFixture: async ({ page }) => {
type IsPage = (typeof page) extends Page ? true : never;
const isPage: IsPage = true;
}
});
base.extend<{ myFixture: (arg: number) => void }>({
// @ts-expect-error
myFixture: (arg: number) => {},
});
base.extend<{ myFixture: (arg: number) => void }>({
myFixture: async (_, use) => {
use((arg: number) => {});
// @ts-expect-error
use((arg: string) => {});
}
});
`,
'playwright.config.ts': `
import { MyOptions } from './helper';

View file

@ -116,6 +116,55 @@ test('should update missing snapshots', async ({ runInlineTest }, testInfo) => {
expect(result2.exitCode).toBe(0);
});
test('should update multiple missing snapshots', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'.git/marker': '',
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
});
`
});
expect(result.exitCode).toBe(0);
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for:
a.spec.ts
git apply test-results/rebaselines.patch
`);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8');
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
--- a/a.spec.ts
+++ b/a.spec.ts
@@ -2,7 +2,11 @@
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello</h1>\`);
- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
- await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`
+ - heading "hello" [level=1]
+ \`);
+ await expect(page.locator('body')).toMatchAriaSnapshot(\`
+ - heading "hello" [level=1]
+ \`);
});
\\ No newline at end of file
`);
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
const result2 = await runInlineTest({});
expect(result2.exitCode).toBe(0);
});
test('should generate baseline with regex', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'.git/marker': '',

View file

@ -78,7 +78,7 @@ export type TestDetails = {
type TestBody<TestArgs> = (args: TestArgs, testInfo: TestInfo) => Promise<void> | void;
type ConditionBody<TestArgs> = (args: TestArgs) => boolean;
export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue> {
export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
(title: string, body: TestBody<TestArgs & WorkerArgs>): void;
(title: string, details: TestDetails, body: TestBody<TestArgs & WorkerArgs>): void;
@ -164,23 +164,22 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void;
step<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>;
expect: Expect<{}>;
extend<T extends KeyValue, W extends KeyValue = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;
extend<T extends {}, W extends {} = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;
info(): TestInfo;
}
type KeyValue = { [key: string]: any };
export type TestFixture<R, Args extends KeyValue> = (args: Args, use: (r: R) => Promise<void>, testInfo: TestInfo) => any;
export type WorkerFixture<R, Args extends KeyValue> = (args: Args, use: (r: R) => Promise<void>, workerInfo: WorkerInfo) => any;
type TestFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | TestFixture<R, Args>;
type WorkerFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | WorkerFixture<R, Args>;
export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extends KeyValue = {}, PW extends KeyValue = {}> = {
export type TestFixture<R, Args extends {}> = (args: Args, use: (r: R) => Promise<void>, testInfo: TestInfo) => any;
export type WorkerFixture<R, Args extends {}> = (args: Args, use: (r: R) => Promise<void>, workerInfo: WorkerInfo) => any;
type TestFixtureValue<R, Args extends {}> = Exclude<R, Function> | TestFixture<R, Args>;
type WorkerFixtureValue<R, Args extends {}> = Exclude<R, Function> | WorkerFixture<R, Args>;
export type Fixtures<T extends {} = {}, W extends {} = {}, PT extends {} = {}, PW extends {} = {}> = {
[K in keyof PW]?: WorkerFixtureValue<PW[K], W & PW> | [WorkerFixtureValue<PW[K], W & PW>, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean }];
} & {
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }];
} & {
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
[K in Exclude<keyof W, keyof PW | keyof PT>]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
} & {
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
[K in Exclude<keyof T, keyof PW | keyof PT>]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
};
type BrowserName = 'chromium' | 'firefox' | 'webkit';
@ -485,8 +484,8 @@ export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig
export function defineConfig<T>(config: PlaywrightTestConfig<T>): PlaywrightTestConfig<T>;
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>): PlaywrightTestConfig<T, W>;
export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig;
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T>;
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T, W>;
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig<T>[]): PlaywrightTestConfig<T>;
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>, ...configs: PlaywrightTestConfig<T, W>[]): PlaywrightTestConfig<T, W>;
type MergedT<List> = List extends [TestType<infer T, any>, ...(infer Rest)] ? T & MergedT<Rest> : {};
type MergedW<List> = List extends [TestType<any, infer W>, ...(infer Rest)] ? W & MergedW<Rest> : {};

View file

@ -94,14 +94,6 @@ Example:
console.log('\nUpdating browser version in browsers.json...');
for (const descriptor of descriptors)
descriptor.browserVersion = browserVersion;
// 4.1 chromium-headless-shell is equal to chromium version.
if (browserName === 'chromium') {
const headlessShellBrowser = await browsersJSON.browsers.find(b => b.name === 'chromium-headless-shell');
headlessShellBrowser.revision = revision;
headlessShellBrowser.browserVersion = browserVersion;
}
fs.writeFileSync(path.join(CORE_PATH, 'browsers.json'), JSON.stringify(browsersJSON, null, 2) + '\n');
}