Merge branch 'main' into sk-branch-4
This commit is contained in:
commit
663f875183
|
|
@ -35,7 +35,7 @@ Coding style is fully defined in [.eslintrc](https://github.com/microsoft/playwr
|
||||||
npm run lint
|
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
|
### Write documentation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# 🎭 Playwright
|
# 🎭 Playwright
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||||
|
|
||||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
||||||
|
|
||||||
| | Linux | macOS | Windows |
|
| | Linux | macOS | Windows |
|
||||||
| :--- | :---: | :---: | :---: |
|
| :--- | :---: | :---: | :---: |
|
||||||
| Chromium <!-- GEN:chromium-version -->132.0.6834.15<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| Chromium <!-- GEN:chromium-version -->132.0.6834.32<!-- 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: |
|
| 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: |
|
| Firefox <!-- GEN:firefox-version -->132.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,41 @@ await page.Clock.PauseAtAsync(DateTime.Parse("2020-02-02"));
|
||||||
await page.Clock.PauseAtAsync("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
|
### param: Clock.pauseAt.time
|
||||||
* langs: js, java
|
* langs: js, java
|
||||||
* since: v1.45
|
* since: v1.45
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ Additional locator to match.
|
||||||
- returns: <[string]>
|
- returns: <[string]>
|
||||||
|
|
||||||
Captures the aria snapshot of the given element.
|
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**
|
**Usage**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -446,7 +446,7 @@ Expected options currently selected.
|
||||||
* since: v1.49
|
* since: v1.49
|
||||||
* langs: python
|
* langs: python
|
||||||
|
|
||||||
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot`].
|
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot#2`].
|
||||||
|
|
||||||
### param: LocatorAssertions.NotToMatchAriaSnapshot.expected
|
### param: LocatorAssertions.NotToMatchAriaSnapshot.expected
|
||||||
* since: v1.49
|
* since: v1.49
|
||||||
|
|
@ -2121,7 +2121,58 @@ Expected options currently selected.
|
||||||
* since: v1.23
|
* 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
|
* since: v1.49
|
||||||
* langs:
|
* langs:
|
||||||
- alias-java: matchesAriaSnapshot
|
- alias-java: matchesAriaSnapshot
|
||||||
|
|
@ -2170,12 +2221,12 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
|
||||||
""");
|
""");
|
||||||
```
|
```
|
||||||
|
|
||||||
### param: LocatorAssertions.toMatchAriaSnapshot.expected
|
### param: LocatorAssertions.toMatchAriaSnapshot#2.expected
|
||||||
* since: v1.49
|
* since: v1.49
|
||||||
- `expected` <string>
|
- `expected` <string>
|
||||||
|
|
||||||
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
|
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%
|
||||||
* since: v1.49
|
* 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
|
* since: v1.49
|
||||||
|
|
|
||||||
|
|
@ -2762,10 +2762,6 @@ This method requires Playwright to be started in a headed mode, with a falsy [`o
|
||||||
|
|
||||||
Returns the PDF buffer.
|
Returns the PDF buffer.
|
||||||
|
|
||||||
:::note
|
|
||||||
Generating a pdf is currently only supported in Chromium headless.
|
|
||||||
:::
|
|
||||||
|
|
||||||
`page.pdf()` generates a pdf of the page with `print` css media. To generate a pdf with `screen` media, call
|
`page.pdf()` generates a pdf of the page with `print` css media. To generate a pdf with `screen` media, call
|
||||||
[`method: Page.emulateMedia`] before calling `page.pdf()`:
|
[`method: Page.emulateMedia`] before calling `page.pdf()`:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ structure of a page, use the [Chrome DevTools Accessibility Pane](https://develo
|
||||||
|
|
||||||
## Snapshot matching
|
## 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
|
structure of the locator scope with a predefined aria snapshot template, helping validate the page's state against
|
||||||
testing requirements.
|
testing requirements.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -454,11 +454,11 @@ jobs:
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
We have a [pre-built Docker image](./docker.md) which can either be used directly, or as a reference to update your existing Docker definitions.
|
We have a [pre-built Docker image](./docker.md) which can either be used directly or as a reference to update your existing Docker definitions.
|
||||||
|
|
||||||
Suggested configuration
|
Suggested configuration
|
||||||
1. Using `--ipc=host` is also recommended when using Chromium. Without it Chromium can run out of memory
|
1. Using `--ipc=host` is also recommended when using Chromium. Without it Chromium can run out of memory
|
||||||
and crash. Learn more about this option in [Docker docs](https://docs.docker.com/engine/reference/run/#ipc-settings---ipc).
|
and crash. Learn more about this option in [Docker docs](https://docs.docker.com/reference/cli/docker/container/run/#ipc).
|
||||||
1. Seeing other weird errors when launching Chromium? Try running your container
|
1. Seeing other weird errors when launching Chromium? Try running your container
|
||||||
with `docker run --cap-add=SYS_ADMIN` when developing locally.
|
with `docker run --cap-add=SYS_ADMIN` when developing locally.
|
||||||
1. Using `--init` Docker flag or [dumb-init](https://github.com/Yelp/dumb-init) is recommended to avoid special
|
1. Using `--init` Docker flag or [dumb-init](https://github.com/Yelp/dumb-init) is recommended to avoid special
|
||||||
|
|
@ -466,7 +466,7 @@ Suggested configuration
|
||||||
|
|
||||||
### Azure Pipelines
|
### Azure Pipelines
|
||||||
|
|
||||||
For Windows or macOS agents, no additional configuration required, just install Playwright and run your tests.
|
For Windows or macOS agents, no additional configuration is required, just install Playwright and run your tests.
|
||||||
|
|
||||||
For Linux agents, you can use [our Docker container](./docker.md) with Azure
|
For Linux agents, you can use [our Docker container](./docker.md) with Azure
|
||||||
Pipelines support [running containerized
|
Pipelines support [running containerized
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,10 @@ The recommended approach is to use `setFixedTime` to set the time to a specific
|
||||||
- `Event.timeStamp`
|
- `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
|
## Test with predefined time
|
||||||
|
|
||||||
Often you only need to fake `Date.now` while keeping the timers going.
|
Often you only need to fake `Date.now` while keeping the timers going.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ Install the VS Code extension and generate tests directly from VS Code. The exte
|
||||||
|
|
||||||
### Record a New Test
|
### Record a New Test
|
||||||
|
|
||||||
To record a test click on the **Record new** button from the Testing sidebar. This will create a `test-1.spec.ts` file as well as open up a browser window.
|
To record a test click on the **Record new** button from the Testing sidebar. This will create a `test-1.spec.ts` file as well as open up a browser window.
|
||||||
|
|
||||||
<img width="1385" alt="record new in vs code" src="https://user-images.githubusercontent.com/13063165/220961665-615d0ab8-3f0b-439c-ad0b-0424d9aa154b.png" />
|
<img width="1385" alt="record new in vs code" src="https://user-images.githubusercontent.com/13063165/220961665-615d0ab8-3f0b-439c-ad0b-0424d9aa154b.png" />
|
||||||
|
|
||||||
|
|
@ -58,16 +58,16 @@ In the test file in VS Code you will see your new generated actions added to you
|
||||||
|
|
||||||
### Generating locators
|
### Generating locators
|
||||||
|
|
||||||
You can generate locators with the test generator.
|
You can generate locators with the test generator.
|
||||||
- Click on the **Pick locator** button form the testing sidebar and then hover over elements in the browser window to see the [locator](./locators.md) highlighted underneath each element.
|
- Click on the **Pick locator** button form the testing sidebar and then hover over elements in the browser window to see the [locator](./locators.md) highlighted underneath each element.
|
||||||
- Click the element you require and it will now show up in the **Pick locator** box in VS Code.
|
- Click the element you require and it will now show up in the **Pick locator** box in VS Code.
|
||||||
- Press <kbd>Enter</kbd> on your keyboard to copy the locator into the clipboard and then paste anywhere in your code. Or press 'escape' if you want to cancel.
|
- Press <kbd>Enter</kbd> on your keyboard to copy the locator into the clipboard and then paste anywhere in your code. Or press 'escape' if you want to cancel.
|
||||||
|
|
||||||
<img width="1641" alt="Pick locators in VS code" src="https://user-images.githubusercontent.com/13063165/220958368-95b03620-3c9b-40a8-be74-01c96ba03cad.png" />
|
<img width="1641" alt="Pick locators in VS code" src="https://user-images.githubusercontent.com/13063165/220958368-95b03620-3c9b-40a8-be74-01c96ba03cad.png" />
|
||||||
|
|
||||||
## Generate tests with the Playwright Inspector
|
## Generate tests with the Playwright Inspector
|
||||||
|
|
||||||
When running the `codegen` command two windows will be opened, a browser window where you interact with the website you wish to test and the Playwright Inspector window where you can record your tests and then copy them into your editor.
|
When running the `codegen` command two windows will be opened, a browser window where you interact with the website you wish to test and the Playwright Inspector window where you can record your tests and then copy them into your editor.
|
||||||
|
|
||||||
### Running Codegen
|
### Running Codegen
|
||||||
|
|
||||||
|
|
@ -128,10 +128,10 @@ When you have finished interacting with the page, press the **record** button to
|
||||||
Use the **clear** button to clear the code to start recording again. Once finished, close the Playwright inspector window or stop the terminal command.
|
Use the **clear** button to clear the code to start recording again. Once finished, close the Playwright inspector window or stop the terminal command.
|
||||||
|
|
||||||
### Generating locators
|
### Generating locators
|
||||||
You can generate [locators](/locators.md) with the test generator.
|
You can generate [locators](/locators.md) with the test generator.
|
||||||
|
|
||||||
* Press the `'Record'` button to stop the recording and the `'Pick Locator'` button will appear.
|
* Press the `'Record'` button to stop the recording and the `'Pick Locator'` button will appear.
|
||||||
* Click on the `'Pick Locator'` button and then hover over elements in the browser window to see the locator highlighted underneath each element.
|
* Click on the `'Pick Locator'` button and then hover over elements in the browser window to see the locator highlighted underneath each element.
|
||||||
* To choose a locator, click on the element you would like to locate and the code for that locator will appear in the field next to the Pick Locator button.
|
* To choose a locator, click on the element you would like to locate and the code for that locator will appear in the field next to the Pick Locator button.
|
||||||
* You can then edit the locator in this field to fine tune it or use the copy button to copy it and paste it into your code.
|
* You can then edit the locator in this field to fine tune it or use the copy button to copy it and paste it into your code.
|
||||||
|
|
||||||
|
|
@ -164,19 +164,19 @@ You can use the test generator to generate tests using emulation so as to genera
|
||||||
Playwright opens a browser window with its viewport set to a specific width and height and is not responsive as tests need to be run under the same conditions. Use the `--viewport` option to generate tests with a different viewport size.
|
Playwright opens a browser window with its viewport set to a specific width and height and is not responsive as tests need to be run under the same conditions. Use the `--viewport` option to generate tests with a different viewport size.
|
||||||
|
|
||||||
```bash js
|
```bash js
|
||||||
npx playwright codegen --viewport-size=800,600 playwright.dev
|
npx playwright codegen --viewport-size="800,600" playwright.dev
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash java
|
```bash java
|
||||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="codegen --viewport-size=800,600 playwright.dev"
|
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="codegen --viewport-size='800,600' playwright.dev"
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash python
|
```bash python
|
||||||
playwright codegen --viewport-size=800,600 playwright.dev
|
playwright codegen --viewport-size="800,600" playwright.dev
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash csharp
|
```bash csharp
|
||||||
pwsh bin/Debug/netX/playwright.ps1 codegen --viewport-size=800,600 playwright.dev
|
pwsh bin/Debug/netX/playwright.ps1 codegen --viewport-size="800,600" playwright.dev
|
||||||
```
|
```
|
||||||
######
|
######
|
||||||
* langs: js
|
* langs: js
|
||||||
|
|
@ -325,7 +325,7 @@ pwsh bin/Debug/netX/playwright.ps1 codegen --timezone="Europe/Rome" --geolocatio
|
||||||
|
|
||||||
### Preserve authenticated state
|
### Preserve authenticated state
|
||||||
|
|
||||||
Run `codegen` with `--save-storage` to save [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) at the end of the session. This is useful to separately record an authentication step and reuse it later when recording more tests.
|
Run `codegen` with `--save-storage` to save [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) at the end of the session. This is useful to separately record an authentication step and reuse it later when recording more tests.
|
||||||
|
|
||||||
```bash js
|
```bash js
|
||||||
npx playwright codegen github.com/microsoft/playwright --save-storage=auth.json
|
npx playwright codegen github.com/microsoft/playwright --save-storage=auth.json
|
||||||
|
|
@ -367,7 +367,7 @@ pwsh bin/Debug/netX/playwright.ps1 codegen github.com/microsoft/playwright --sav
|
||||||
|
|
||||||
#### Login
|
#### Login
|
||||||
|
|
||||||
After performing authentication and closing the browser, `auth.json` will contain the storage state which you can then reuse in your tests.
|
After performing authentication and closing the browser, `auth.json` will contain the storage state which you can then reuse in your tests.
|
||||||
|
|
||||||
<img width="1394" alt="login to GitHub screen" src="https://user-images.githubusercontent.com/13063165/220561688-04b2b984-4ba6-4446-8b0a-8058876e2a02.png" />
|
<img width="1394" alt="login to GitHub screen" src="https://user-images.githubusercontent.com/13063165/220561688-04b2b984-4ba6-4446-8b0a-8058876e2a02.png" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,8 +115,7 @@ await page.GotoAsync("https://example.com");
|
||||||
You can configure pages to load over the HTTP(S) proxy or SOCKSv5. Proxy can be either set globally
|
You can configure pages to load over the HTTP(S) proxy or SOCKSv5. Proxy can be either set globally
|
||||||
for the entire browser, or for each browser context individually.
|
for the entire browser, or for each browser context individually.
|
||||||
|
|
||||||
You can optionally specify username and password for HTTP(S) proxy, you can also specify hosts to
|
You can optionally specify username and password for HTTP(S) proxy, you can also specify hosts to bypass the [`option: Browser.newContext.proxy`] for.
|
||||||
bypass proxy for.
|
|
||||||
|
|
||||||
Here is an example of a global proxy:
|
Here is an example of a global proxy:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ toc_max_heading_level: 2
|
||||||
|
|
||||||
### Aria snapshots
|
### 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
|
```csharp
|
||||||
await page.GotoAsync("https://playwright.dev");
|
await page.GotoAsync("https://playwright.dev");
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ toc_max_heading_level: 2
|
||||||
|
|
||||||
### Aria snapshots
|
### 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
|
```java
|
||||||
page.navigate("https://playwright.dev");
|
page.navigate("https://playwright.dev");
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
|
||||||
|
|
||||||
### Aria snapshots
|
### 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
|
```js
|
||||||
await page.goto('https://playwright.dev');
|
await page.goto('https://playwright.dev');
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ toc_max_heading_level: 2
|
||||||
|
|
||||||
### Aria snapshots
|
### 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
|
```python
|
||||||
page.goto("https://playwright.dev")
|
page.goto("https://playwright.dev")
|
||||||
|
|
|
||||||
|
|
@ -392,8 +392,11 @@ export default defineConfig({
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## property: TestOptions.locale = %%-context-option-locale-%%
|
## property: TestOptions.locale
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
|
- type: <[string]>
|
||||||
|
|
||||||
|
Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value as well as number and date formatting rules. Defaults to `en-US`. Learn more about emulation in our [emulation guide](../emulation.md#locale--timezone).
|
||||||
|
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -259,3 +259,18 @@ def test_bing_is_working(page):
|
||||||
## Deploy to CI
|
## Deploy to CI
|
||||||
|
|
||||||
See the [guides for CI providers](./ci.md) to deploy your tests to CI/CD.
|
See the [guides for CI providers](./ci.md) to deploy your tests to CI/CD.
|
||||||
|
|
||||||
|
## Async Fixtures
|
||||||
|
|
||||||
|
If you want to use async fixtures, you can use the [`pytest-playwright-asyncio`](https://pypi.org/project/pytest-playwright-asyncio/) plugin.
|
||||||
|
Make sure to use `pytest-asyncio>=0.24.0` and make your tests use of [`loop_scope=sesion`](https://pytest-asyncio.readthedocs.io/en/latest/how-to-guides/run_session_tests_in_same_loop.html).
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
from playwright.async_api import Page
|
||||||
|
|
||||||
|
@pytest.mark.asyncio(loop_scope="session")
|
||||||
|
async def test_foo(page: Page):
|
||||||
|
await page.goto("https://github.com")
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ When `fullyParallel: true` is enabled, Playwright Test runs individual tests in
|
||||||
|
|
||||||
**Sharding without fullyParallel**
|
**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:**
|
**Key Takeaways:**
|
||||||
|
|
||||||
|
|
|
||||||
20
package-lock.json
generated
20
package-lock.json
generated
|
|
@ -2171,14 +2171,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
"version": "4.6.2",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.0.tgz",
|
||||||
"integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
|
"integrity": "sha512-7n7KdUEtx/7Yl7I/WVAMZ1bEb0eVvXF3ummWTeLcs/9gvo9pJhuLdouSXGjdZ/MKD1acf1I272+X0RMua4/R3g==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.18.0 || >=16.0.0"
|
"node": "^18.0.0 || >=20.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vite": "^4.0.0 || ^5.0.0",
|
"vite": "^5.0.0",
|
||||||
"vue": "^3.2.25"
|
"vue": "^3.2.25"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -5395,15 +5396,16 @@
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.cjs"
|
||||||
},
|
},
|
||||||
|
|
@ -7876,7 +7878,7 @@
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||||
"@vitejs/plugin-vue": "^4.2.1"
|
"@vitejs/plugin-vue": "^5.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,21 @@
|
||||||
"browsers": [
|
"browsers": [
|
||||||
{
|
{
|
||||||
"name": "chromium",
|
"name": "chromium",
|
||||||
"revision": "1150",
|
"revision": "1151",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"browserVersion": "132.0.6834.15"
|
"browserVersion": "132.0.6834.32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "chromium-headless-shell",
|
"name": "chromium-headless-shell",
|
||||||
"revision": "1150",
|
"revision": "1151",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"browserVersion": "132.0.6834.15"
|
"browserVersion": "132.0.6834.32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "chromium-tip-of-tree",
|
"name": "chromium-tip-of-tree",
|
||||||
"revision": "1280",
|
"revision": "1284",
|
||||||
"installByDefault": false,
|
"installByDefault": false,
|
||||||
"browserVersion": "133.0.6850.0"
|
"browserVersion": "133.0.6878.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "firefox",
|
"name": "firefox",
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "webkit",
|
"name": "webkit",
|
||||||
"revision": "2110",
|
"revision": "2118",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"revisionOverrides": {
|
"revisionOverrides": {
|
||||||
"debian11-x64": "2105",
|
"debian11-x64": "2105",
|
||||||
|
|
|
||||||
|
|
@ -449,10 +449,12 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
||||||
// Viewport size
|
// Viewport size
|
||||||
if (options.viewportSize) {
|
if (options.viewportSize) {
|
||||||
try {
|
try {
|
||||||
const [width, height] = options.viewportSize.split(',').map(n => parseInt(n, 10));
|
const [width, height] = options.viewportSize.split(',').map(n => +n);
|
||||||
|
if (isNaN(width) || isNaN(height))
|
||||||
|
throw new Error('bad values');
|
||||||
contextOptions.viewport = { width, height };
|
contextOptions.viewport = { width, height };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('Invalid viewport size format: use "width, height", for example --viewport-size=800,600');
|
throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -846,6 +846,9 @@ class FrameSession {
|
||||||
event.type,
|
event.type,
|
||||||
event.message,
|
event.message,
|
||||||
async (accept: boolean, promptText?: string) => {
|
async (accept: boolean, promptText?: string) => {
|
||||||
|
// TODO: this should actually be a CDP event that notifies about a cancelled navigation attempt.
|
||||||
|
if (this._isMainFrame() && event.type === 'beforeunload' && !accept)
|
||||||
|
this._page._frameManager.frameAbortedNavigation(this._page.mainFrame()._id, 'navigation cancelled by beforeunload dialog');
|
||||||
await this._client.send('Page.handleJavaScriptDialog', { accept, promptText });
|
await this._client.send('Page.handleJavaScriptDialog', { accept, promptText });
|
||||||
},
|
},
|
||||||
event.defaultPrompt));
|
event.defaultPrompt));
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Galaxy S5": {
|
"Galaxy S5": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -121,7 +121,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S5 landscape": {
|
"Galaxy S5 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -132,7 +132,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S8": {
|
"Galaxy S8": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 740
|
"height": 740
|
||||||
|
|
@ -143,7 +143,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S8 landscape": {
|
"Galaxy S8 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 740,
|
"width": 740,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -154,7 +154,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S9+": {
|
"Galaxy S9+": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 320,
|
"width": 320,
|
||||||
"height": 658
|
"height": 658
|
||||||
|
|
@ -165,7 +165,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S9+ landscape": {
|
"Galaxy S9+ landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 658,
|
"width": 658,
|
||||||
"height": 320
|
"height": 320
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy Tab S4": {
|
"Galaxy Tab S4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Safari/537.36",
|
"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",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 712,
|
"width": 712,
|
||||||
"height": 1138
|
"height": 1138
|
||||||
|
|
@ -187,7 +187,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy Tab S4 landscape": {
|
"Galaxy Tab S4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Safari/537.36",
|
"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",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 1138,
|
"width": 1138,
|
||||||
"height": 712
|
"height": 712
|
||||||
|
|
@ -1098,7 +1098,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"LG Optimus L70": {
|
"LG Optimus L70": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 384,
|
"width": 384,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1109,7 +1109,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"LG Optimus L70 landscape": {
|
"LG Optimus L70 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 384
|
"height": 384
|
||||||
|
|
@ -1120,7 +1120,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 550": {
|
"Microsoft Lumia 550": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1131,7 +1131,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 550 landscape": {
|
"Microsoft Lumia 550 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1142,7 +1142,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 950": {
|
"Microsoft Lumia 950": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1153,7 +1153,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 950 landscape": {
|
"Microsoft Lumia 950 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1164,7 +1164,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 10": {
|
"Nexus 10": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 800,
|
"width": 800,
|
||||||
"height": 1280
|
"height": 1280
|
||||||
|
|
@ -1175,7 +1175,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 10 landscape": {
|
"Nexus 10 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 800
|
"height": 800
|
||||||
|
|
@ -1186,7 +1186,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 4": {
|
"Nexus 4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 384,
|
"width": 384,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1197,7 +1197,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 4 landscape": {
|
"Nexus 4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 384
|
"height": 384
|
||||||
|
|
@ -1208,7 +1208,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5": {
|
"Nexus 5": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1219,7 +1219,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5 landscape": {
|
"Nexus 5 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1230,7 +1230,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5X": {
|
"Nexus 5X": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1241,7 +1241,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5X landscape": {
|
"Nexus 5X landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1252,7 +1252,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6": {
|
"Nexus 6": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1263,7 +1263,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6 landscape": {
|
"Nexus 6 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1274,7 +1274,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6P": {
|
"Nexus 6P": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1285,7 +1285,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6P landscape": {
|
"Nexus 6P landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1296,7 +1296,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 7": {
|
"Nexus 7": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 960
|
"height": 960
|
||||||
|
|
@ -1307,7 +1307,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 7 landscape": {
|
"Nexus 7 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 960,
|
"width": 960,
|
||||||
"height": 600
|
"height": 600
|
||||||
|
|
@ -1362,7 +1362,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Pixel 2": {
|
"Pixel 2": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 411,
|
"width": 411,
|
||||||
"height": 731
|
"height": 731
|
||||||
|
|
@ -1373,7 +1373,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 landscape": {
|
"Pixel 2 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 731,
|
"width": 731,
|
||||||
"height": 411
|
"height": 411
|
||||||
|
|
@ -1384,7 +1384,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 XL": {
|
"Pixel 2 XL": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 411,
|
"width": 411,
|
||||||
"height": 823
|
"height": 823
|
||||||
|
|
@ -1395,7 +1395,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 XL landscape": {
|
"Pixel 2 XL landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 823,
|
"width": 823,
|
||||||
"height": 411
|
"height": 411
|
||||||
|
|
@ -1406,7 +1406,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 3": {
|
"Pixel 3": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 393,
|
"width": 393,
|
||||||
"height": 786
|
"height": 786
|
||||||
|
|
@ -1417,7 +1417,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 3 landscape": {
|
"Pixel 3 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 786,
|
"width": 786,
|
||||||
"height": 393
|
"height": 393
|
||||||
|
|
@ -1428,7 +1428,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4": {
|
"Pixel 4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 353,
|
"width": 353,
|
||||||
"height": 745
|
"height": 745
|
||||||
|
|
@ -1439,7 +1439,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4 landscape": {
|
"Pixel 4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 745,
|
"width": 745,
|
||||||
"height": 353
|
"height": 353
|
||||||
|
|
@ -1450,7 +1450,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4a (5G)": {
|
"Pixel 4a (5G)": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
"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",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 892
|
"height": 892
|
||||||
|
|
@ -1465,7 +1465,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4a (5G) landscape": {
|
"Pixel 4a (5G) landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
"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",
|
||||||
"screen": {
|
"screen": {
|
||||||
"height": 892,
|
"height": 892,
|
||||||
"width": 412
|
"width": 412
|
||||||
|
|
@ -1480,7 +1480,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 5": {
|
"Pixel 5": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.32 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 393,
|
"width": 393,
|
||||||
"height": 851
|
"height": 851
|
||||||
|
|
@ -1495,7 +1495,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 5 landscape": {
|
"Pixel 5 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.32 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 851,
|
"width": 851,
|
||||||
"height": 393
|
"height": 393
|
||||||
|
|
@ -1510,7 +1510,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 7": {
|
"Pixel 7": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.32 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 915
|
"height": 915
|
||||||
|
|
@ -1525,7 +1525,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 7 landscape": {
|
"Pixel 7 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.32 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 915,
|
"width": 915,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1540,7 +1540,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Moto G4": {
|
"Moto G4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1551,7 +1551,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Moto G4 landscape": {
|
"Moto G4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 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.32 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1562,7 +1562,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Chrome HiDPI": {
|
"Desktop Chrome HiDPI": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.32 Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1792,
|
"width": 1792,
|
||||||
"height": 1120
|
"height": 1120
|
||||||
|
|
@ -1577,7 +1577,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Edge HiDPI": {
|
"Desktop Edge HiDPI": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Safari/537.36 Edg/132.0.6834.15",
|
"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",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1792,
|
"width": 1792,
|
||||||
"height": 1120
|
"height": 1120
|
||||||
|
|
@ -1622,7 +1622,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Desktop Chrome": {
|
"Desktop Chrome": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.32 Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080
|
||||||
|
|
@ -1637,7 +1637,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Edge": {
|
"Desktop Edge": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Safari/537.36 Edg/132.0.6834.15",
|
"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",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080
|
||||||
|
|
|
||||||
|
|
@ -158,8 +158,10 @@ function toAriaNode(element: Element): AriaNode | null {
|
||||||
if (roleUtils.kAriaSelectedRoles.includes(role))
|
if (roleUtils.kAriaSelectedRoles.includes(role))
|
||||||
result.selected = roleUtils.getAriaSelected(element);
|
result.selected = roleUtils.getAriaSelected(element);
|
||||||
|
|
||||||
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)
|
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
||||||
result.children = [element.value];
|
if (element.type !== 'checkbox' && element.type !== 'radio')
|
||||||
|
result.children = [element.value];
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1157,7 +1157,6 @@ export class Recorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
clearHighlight() {
|
clearHighlight() {
|
||||||
this._currentTool.cleanup?.();
|
|
||||||
this.updateHighlight(null, false);
|
this.updateHighlight(null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ function buildNoTextCandidates(injectedScript: InjectedScript, element: Element,
|
||||||
if (input.placeholder) {
|
if (input.placeholder) {
|
||||||
candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder, true)}]`, score: kPlaceholderScoreExact });
|
candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder, true)}]`, score: kPlaceholderScoreExact });
|
||||||
for (const alternative of suitableTextAlternatives(input.placeholder))
|
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;
|
const labelText = label.normalized;
|
||||||
candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(labelText, true), score: kLabelScoreExact });
|
candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(labelText, true), score: kLabelScoreExact });
|
||||||
for (const alternative of suitableTextAlternatives(labelText))
|
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);
|
const ariaRole = getAriaRole(element);
|
||||||
|
|
@ -308,28 +308,28 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
|
||||||
if (title) {
|
if (title) {
|
||||||
candidates.push([{ engine: 'internal:attr', selector: `[title=${escapeForAttributeSelector(title, true)}]`, score: kTitleScoreExact }]);
|
candidates.push([{ engine: 'internal:attr', selector: `[title=${escapeForAttributeSelector(title, true)}]`, score: kTitleScoreExact }]);
|
||||||
for (const alternative of suitableTextAlternatives(title))
|
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');
|
const alt = element.getAttribute('alt');
|
||||||
if (alt && ['APPLET', 'AREA', 'IMG', 'INPUT'].includes(element.nodeName)) {
|
if (alt && ['APPLET', 'AREA', 'IMG', 'INPUT'].includes(element.nodeName)) {
|
||||||
candidates.push([{ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(alt, true)}]`, score: kAltTextScoreExact }]);
|
candidates.push([{ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(alt, true)}]`, score: kAltTextScoreExact }]);
|
||||||
for (const alternative of suitableTextAlternatives(alt))
|
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 text = elementText(injectedScript._evaluator._cacheText, element).normalized;
|
||||||
|
const textAlternatives = text ? suitableTextAlternatives(text) : [];
|
||||||
if (text) {
|
if (text) {
|
||||||
const alternatives = suitableTextAlternatives(text);
|
|
||||||
if (isTargetNode) {
|
if (isTargetNode) {
|
||||||
if (text.length <= 80)
|
if (text.length <= 80)
|
||||||
candidates.push([{ engine: 'internal:text', selector: escapeForTextSelector(text, true), score: kTextScoreExact }]);
|
candidates.push([{ engine: 'internal:text', selector: escapeForTextSelector(text, true), score: kTextScoreExact }]);
|
||||||
for (const alternative of alternatives)
|
for (const alternative of textAlternatives)
|
||||||
candidates.push([{ engine: 'internal:text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBouns }]);
|
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 };
|
const cssToken: SelectorToken = { engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: kCSSTagNameScore };
|
||||||
for (const alternative of alternatives)
|
for (const alternative of textAlternatives)
|
||||||
candidates.push([cssToken, { engine: 'internal:has-text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBouns }]);
|
candidates.push([cssToken, { engine: 'internal:has-text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBonus }]);
|
||||||
if (text.length <= 80) {
|
if (text.length <= 80) {
|
||||||
const re = new RegExp('^' + escapeRegExp(text) + '$');
|
const re = new RegExp('^' + escapeRegExp(text) + '$');
|
||||||
candidates.push([cssToken, { engine: 'internal:has-text', selector: escapeForTextSelector(re, false), score: kTextScoreRegex }]);
|
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)) {
|
if (ariaRole && !['none', 'presentation'].includes(ariaRole)) {
|
||||||
const ariaName = getElementAccessibleName(element, false);
|
const ariaName = getElementAccessibleName(element, false);
|
||||||
if (ariaName) {
|
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))
|
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) {
|
function suitableTextAlternatives(text: string) {
|
||||||
let result: { text: string, scoreBouns: number }[] = [];
|
let result: { text: string, scoreBonus: number }[] = [];
|
||||||
|
|
||||||
{
|
{
|
||||||
const match = text.match(/^([\d.,]+)[^.,\w]/);
|
const match = text.match(/^([\d.,]+)[^.,\w]/);
|
||||||
const leadingNumberLength = match ? match[1].length : 0;
|
const leadingNumberLength = match ? match[1].length : 0;
|
||||||
if (leadingNumberLength) {
|
if (leadingNumberLength) {
|
||||||
const alt = trimWordBoundary(text.substring(leadingNumberLength).trimStart(), 80);
|
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;
|
const trailingNumberLength = match ? match[1].length : 0;
|
||||||
if (trailingNumberLength) {
|
if (trailingNumberLength) {
|
||||||
const alt = trimWordBoundary(text.substring(0, text.length - trailingNumberLength).trimEnd(), 80);
|
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) {
|
if (text.length <= 30) {
|
||||||
result.push({ text, scoreBouns: 0 });
|
result.push({ text, scoreBonus: 0 });
|
||||||
} else {
|
} else {
|
||||||
result.push({ text: trimWordBoundary(text, 80), scoreBouns: 0 });
|
result.push({ text: trimWordBoundary(text, 80), scoreBonus: 0 });
|
||||||
result.push({ text: trimWordBoundary(text, 30), scoreBouns: 1 });
|
result.push({ text: trimWordBoundary(text, 30), scoreBonus: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
result = result.filter(r => r.text);
|
result = result.filter(r => r.text);
|
||||||
if (!result.length)
|
if (!result.length)
|
||||||
result.push({ text: text.substring(0, 80), scoreBouns: 0 });
|
result.push({ text: text.substring(0, 80), scoreBonus: 0 });
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,12 +62,8 @@ function yamlStringNeedsQuotes(str: string): boolean {
|
||||||
if (/^-\s/.test(str))
|
if (/^-\s/.test(str))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Strings that start with a special indicator character need quotes
|
// Strings containing ':' or '\n' followed by a space or at the end need quotes
|
||||||
if (/^[&*\],].*/.test(str))
|
if (/[\n:](\s|$)/.test(str))
|
||||||
return true;
|
|
||||||
|
|
||||||
// Strings containing ':' followed by a space or at the end need quotes
|
|
||||||
if (/:(\s|$)/.test(str))
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Strings containing '#' preceded by a space need quotes (comment indicator)
|
// Strings containing '#' preceded by a space need quotes (comment indicator)
|
||||||
|
|
@ -78,21 +74,17 @@ function yamlStringNeedsQuotes(str: string): boolean {
|
||||||
if (/[\n\r]/.test(str))
|
if (/[\n\r]/.test(str))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Strings starting with '?' or '!' (directives) need quotes
|
// Strings starting with indicator characters or quotes need quotes
|
||||||
if (/^[?!]/.test(str))
|
if (/^[&*\],?!>|@"'#%]/.test(str))
|
||||||
return true;
|
|
||||||
|
|
||||||
// Strings starting with '>' or '|' (block scalar indicators) need quotes
|
|
||||||
if (/^[>|]/.test(str))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// Strings starting with quotes need quotes
|
|
||||||
if (/^["']/.test(str))
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Strings containing special characters that could cause ambiguity
|
// Strings containing special characters that could cause ambiguity
|
||||||
if (/[{}`]/.test(str))
|
if (/[{}`]/.test(str))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
// Non-string types recognized by YAML
|
||||||
|
if (!isNaN(Number(str)) || ['y', 'n', 'yes', 'no', 'true', 'false', 'on', 'off', 'null'].includes(str.toLowerCase()))
|
||||||
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export class RecorderCollection extends EventEmitter {
|
||||||
|
|
||||||
restart() {
|
restart() {
|
||||||
this._actions = [];
|
this._actions = [];
|
||||||
this._fireChange();
|
this.emit('change', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
setEnabled(enabled: boolean) {
|
setEnabled(enabled: boolean) {
|
||||||
|
|
@ -128,6 +128,7 @@ export class RecorderCollection extends EventEmitter {
|
||||||
private _fireChange() {
|
private _fireChange() {
|
||||||
if (!this._enabled)
|
if (!this._enabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.emit('change', collapseActions(this._actions));
|
this.emit('change', collapseActions(this._actions));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-
|
||||||
import type { HeadersArray } from './types';
|
import type { HeadersArray } from './types';
|
||||||
|
|
||||||
export const perMessageDeflate = {
|
export const perMessageDeflate = {
|
||||||
|
clientNoContextTakeover: true,
|
||||||
zlibDeflateOptions: {
|
zlibDeflateOptions: {
|
||||||
level: 3,
|
level: 3,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -611,6 +611,9 @@ export class WKPage implements PageDelegate {
|
||||||
event.type as dialog.DialogType,
|
event.type as dialog.DialogType,
|
||||||
event.message,
|
event.message,
|
||||||
async (accept: boolean, promptText?: string) => {
|
async (accept: boolean, promptText?: string) => {
|
||||||
|
// TODO: this should actually be a RDP event that notifies about a cancelled navigation attempt.
|
||||||
|
if (event.type === 'beforeunload' && !accept)
|
||||||
|
this._page._frameManager.frameAbortedNavigation(this._page.mainFrame()._id, 'navigation cancelled by beforeunload dialog');
|
||||||
await this._pageProxySession.send('Dialog.handleJavaScriptDialog', { accept, promptText });
|
await this._pageProxySession.send('Dialog.handleJavaScriptDialog', { accept, promptText });
|
||||||
},
|
},
|
||||||
event.defaultPrompt));
|
event.defaultPrompt));
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ let lastConnectionId = 0;
|
||||||
const kConnectionSymbol = Symbol('kConnection');
|
const kConnectionSymbol = Symbol('kConnection');
|
||||||
|
|
||||||
export const perMessageDeflate = {
|
export const perMessageDeflate = {
|
||||||
|
serverNoContextTakeover: true,
|
||||||
zlibDeflateOptions: {
|
zlibDeflateOptions: {
|
||||||
level: 3,
|
level: 3,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
17
packages/playwright-core/types/types.d.ts
vendored
17
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -3609,8 +3609,6 @@ export interface Page {
|
||||||
/**
|
/**
|
||||||
* Returns the PDF buffer.
|
* Returns the PDF buffer.
|
||||||
*
|
*
|
||||||
* **NOTE** Generating a pdf is currently only supported in Chromium headless.
|
|
||||||
*
|
|
||||||
* `page.pdf()` generates a pdf of the page with `print` css media. To generate a pdf with `screen` media, call
|
* `page.pdf()` generates a pdf of the page with `print` css media. To generate a pdf with `screen` media, call
|
||||||
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) before calling
|
* [page.emulateMedia([options])](https://playwright.dev/docs/api/class-page#page-emulate-media) before calling
|
||||||
* `page.pdf()`:
|
* `page.pdf()`:
|
||||||
|
|
@ -12430,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
|
* 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.
|
* for the corresponding assertion.
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
|
|
@ -18592,6 +18590,19 @@ export interface Clock {
|
||||||
* await page.clock.pauseAt('2020-02-02');
|
* 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.
|
* @param time Time to pause at.
|
||||||
*/
|
*/
|
||||||
pauseAt(time: number|string|Date): Promise<void>;
|
pauseAt(time: number|string|Date): Promise<void>;
|
||||||
|
|
|
||||||
4
packages/playwright-ct-core/index.d.ts
vendored
4
packages/playwright-ct-core/index.d.ts
vendored
|
|
@ -52,7 +52,7 @@ export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig
|
||||||
export function defineConfig<T>(config: PlaywrightTestConfig<T>): PlaywrightTestConfig<T>;
|
export function defineConfig<T>(config: PlaywrightTestConfig<T>): PlaywrightTestConfig<T>;
|
||||||
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>): PlaywrightTestConfig<T, W>;
|
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>): PlaywrightTestConfig<T, W>;
|
||||||
export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig;
|
export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig;
|
||||||
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T>;
|
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig<T>[]): PlaywrightTestConfig<T>;
|
||||||
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T, W>;
|
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>, ...configs: PlaywrightTestConfig<T, W>[]): PlaywrightTestConfig<T, W>;
|
||||||
|
|
||||||
export { expect, devices, Locator } from 'playwright/test';
|
export { expect, devices, Locator } from 'playwright/test';
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||||
"@vitejs/plugin-vue": "^4.2.1"
|
"@vitejs/plugin-vue": "^5.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,25 @@
|
||||||
import type { LocatorEx } from './matchers';
|
import type { LocatorEx } from './matchers';
|
||||||
import type { ExpectMatcherState } from '../../types/test';
|
import type { ExpectMatcherState } from '../../types/test';
|
||||||
import { kNoElementsFoundError, matcherHint, type MatcherResult } from './matcherHint';
|
import { kNoElementsFoundError, matcherHint, type MatcherResult } from './matcherHint';
|
||||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
|
||||||
import { EXPECTED_COLOR } from '../common/expectBundle';
|
import { EXPECTED_COLOR } from '../common/expectBundle';
|
||||||
import { callLogText } from '../util';
|
import { callLogText, sanitizeFilePathBeforeExtension, trimLongString } from '../util';
|
||||||
import { printReceivedStringContainExpectedSubstring } from './expect';
|
import { printReceivedStringContainExpectedSubstring } from './expect';
|
||||||
import { currentTestInfo } from '../common/globals';
|
import { currentTestInfo } from '../common/globals';
|
||||||
import type { MatcherReceived } from '@injected/ariaSnapshot';
|
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(
|
export async function toMatchAriaSnapshot(
|
||||||
this: ExpectMatcherState,
|
this: ExpectMatcherState,
|
||||||
receiver: LocatorEx,
|
receiver: LocatorEx,
|
||||||
expected: string,
|
expectedParam: ToMatchAriaSnapshotExpected,
|
||||||
options: { timeout?: number, matchSubstring?: boolean } = {},
|
options: { timeout?: number } = {},
|
||||||
): Promise<MatcherResult<string | RegExp, string>> {
|
): Promise<MatcherResult<string | RegExp, string>> {
|
||||||
const matcherName = 'toMatchAriaSnapshot';
|
const matcherName = 'toMatchAriaSnapshot';
|
||||||
|
|
||||||
|
|
@ -39,7 +45,7 @@ export async function toMatchAriaSnapshot(
|
||||||
throw new Error(`toMatchAriaSnapshot() must be called during the test`);
|
throw new Error(`toMatchAriaSnapshot() must be called during the test`);
|
||||||
|
|
||||||
if (testInfo._projectInternal.ignoreSnapshots)
|
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;
|
const updateSnapshots = testInfo.config.updateSnapshots;
|
||||||
|
|
||||||
|
|
@ -48,12 +54,25 @@ export async function toMatchAriaSnapshot(
|
||||||
promise: this.promise,
|
promise: this.promise,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof expected !== 'string') {
|
let expected: string;
|
||||||
throw new Error([
|
let expectedPath: string | undefined;
|
||||||
matcherHint(this, receiver, matcherName, receiver, expected, matcherOptions),
|
if (isString(expectedParam)) {
|
||||||
`${colors.bold('Matcher error')}: ${EXPECTED_COLOR('expected',)} value must be a string`,
|
expected = expectedParam;
|
||||||
this.utils.printWithType('Expected', expected, this.utils.printExpected)
|
} else {
|
||||||
].join('\n\n'));
|
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;
|
const generateMissingBaseline = updateSnapshots === 'missing' && !expected;
|
||||||
|
|
@ -102,8 +121,24 @@ export async function toMatchAriaSnapshot(
|
||||||
if ((updateSnapshots === 'all') ||
|
if ((updateSnapshots === 'all') ||
|
||||||
(updateSnapshots === 'changed' && pass === this.isNot) ||
|
(updateSnapshots === 'changed' && pass === this.isNot) ||
|
||||||
generateMissingBaseline) {
|
generateMissingBaseline) {
|
||||||
const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\`)`;
|
if (expectedPath) {
|
||||||
return { pass: this.isNot, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
|
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 {
|
function indent(snapshot: string, indent: string): string {
|
||||||
return snapshot.split('\n').map(line => indent + line).join('\n');
|
return snapshot.split('\n').map(line => indent + line).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const snapshotNamesSymbol = Symbol('snapshotNames');
|
||||||
|
|
||||||
|
type SnapshotNames = {
|
||||||
|
anonymousSnapshotIndex: number;
|
||||||
|
};
|
||||||
|
|
|
||||||
58
packages/playwright/types/test.d.ts
vendored
58
packages/playwright/types/test.d.ts
vendored
|
|
@ -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.
|
* Declares a test.
|
||||||
* - `test(title, body)`
|
* - `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).
|
* 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).
|
* @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,
|
* Returns information about the currently running test. This method can only be called during the test execution,
|
||||||
* otherwise it throws.
|
* otherwise it throws.
|
||||||
|
|
@ -5653,19 +5653,18 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
|
||||||
info(): TestInfo;
|
info(): TestInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyValue = { [key: string]: any };
|
export type TestFixture<R, Args extends {}> = (args: Args, use: (r: R) => Promise<void>, testInfo: TestInfo) => any;
|
||||||
export type TestFixture<R, Args extends KeyValue> = (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;
|
||||||
export type WorkerFixture<R, Args extends KeyValue> = (args: Args, use: (r: R) => Promise<void>, workerInfo: WorkerInfo) => any;
|
type TestFixtureValue<R, Args extends {}> = Exclude<R, Function> | TestFixture<R, Args>;
|
||||||
type TestFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | TestFixture<R, Args>;
|
type WorkerFixtureValue<R, Args extends {}> = Exclude<R, Function> | WorkerFixture<R, Args>;
|
||||||
type WorkerFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | WorkerFixture<R, Args>;
|
export type Fixtures<T extends {} = {}, W extends {} = {}, PT extends {} = {}, PW extends {} = {}> = {
|
||||||
export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extends KeyValue = {}, PW extends KeyValue = {}> = {
|
|
||||||
[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 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 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';
|
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||||
|
|
@ -6247,8 +6246,8 @@ export interface PlaywrightTestOptions {
|
||||||
javaScriptEnabled: boolean;
|
javaScriptEnabled: boolean;
|
||||||
/**
|
/**
|
||||||
* Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value,
|
* Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value,
|
||||||
* `Accept-Language` request header value as well as number and date formatting rules. Defaults to the system default
|
* `Accept-Language` request header value as well as number and date formatting rules. Defaults to `en-US`. Learn more
|
||||||
* locale. Learn more about emulation in our [emulation guide](https://playwright.dev/docs/emulation#locale--timezone).
|
* about emulation in our [emulation guide](https://playwright.dev/docs/emulation#locale--timezone).
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
*
|
*
|
||||||
|
|
@ -7487,8 +7486,8 @@ export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig
|
||||||
export function defineConfig<T>(config: PlaywrightTestConfig<T>): PlaywrightTestConfig<T>;
|
export function defineConfig<T>(config: PlaywrightTestConfig<T>): PlaywrightTestConfig<T>;
|
||||||
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>): PlaywrightTestConfig<T, W>;
|
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>): PlaywrightTestConfig<T, W>;
|
||||||
export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig;
|
export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig;
|
||||||
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T>;
|
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig<T>[]): PlaywrightTestConfig<T>;
|
||||||
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T, W>;
|
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 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> : {};
|
type MergedW<List> = List extends [TestType<any, infer W>, ...(infer Rest)] ? W & MergedW<Rest> : {};
|
||||||
|
|
@ -8431,6 +8430,37 @@ interface LocatorAssertions {
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
}): Promise<void>;
|
}): 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).
|
* Asserts that the target element matches the given [accessibility snapshot](https://playwright.dev/docs/aria-snapshots).
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -319,7 +319,9 @@ function indexTree<T extends TreeItem>(
|
||||||
selectedItem: T | undefined,
|
selectedItem: T | undefined,
|
||||||
expandedItems: Map<string, boolean | undefined>,
|
expandedItems: Map<string, boolean | undefined>,
|
||||||
autoExpandDepth: number,
|
autoExpandDepth: number,
|
||||||
isVisible?: (item: T) => boolean): Map<T, TreeItemData> {
|
isVisible: (item: T) => boolean = () => true): Map<T, TreeItemData> {
|
||||||
|
if (!isVisible(rootItem))
|
||||||
|
return new Map();
|
||||||
|
|
||||||
const result = new Map<T, TreeItemData>();
|
const result = new Map<T, TreeItemData>();
|
||||||
const temporaryExpanded = new Set<string>();
|
const temporaryExpanded = new Set<string>();
|
||||||
|
|
@ -328,9 +330,9 @@ function indexTree<T extends TreeItem>(
|
||||||
let lastItem: T | null = null;
|
let lastItem: T | null = null;
|
||||||
|
|
||||||
const appendChildren = (parent: T, depth: number) => {
|
const appendChildren = (parent: T, depth: number) => {
|
||||||
if (isVisible && !isVisible(parent))
|
|
||||||
return;
|
|
||||||
for (const item of parent.children as T[]) {
|
for (const item of parent.children as T[]) {
|
||||||
|
if (!isVisible(item))
|
||||||
|
continue;
|
||||||
const expandState = temporaryExpanded.has(item.id) || expandedItems.get(item.id);
|
const expandState = temporaryExpanded.has(item.id) || expandedItems.get(item.id);
|
||||||
const autoExpandMatches = autoExpandDepth > depth && result.size < 25 && expandState !== false;
|
const autoExpandMatches = autoExpandDepth > depth && result.size < 25 && expandState !== false;
|
||||||
const expanded = item.children.length ? expandState ?? autoExpandMatches : undefined;
|
const expanded = item.children.length ? expandState ?? autoExpandMatches : undefined;
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ test('render an empty component', async ({ mount, page }) => {
|
||||||
const testWithServer = test.extend(serverFixtures);
|
const testWithServer = test.extend(serverFixtures);
|
||||||
testWithServer(
|
testWithServer(
|
||||||
'components routing should go through context',
|
'components routing should go through context',
|
||||||
|
// @ts-ignore "serverFixtures" are imported from the impl without any types
|
||||||
async ({ mount, context, server }) => {
|
async ({ mount, context, server }) => {
|
||||||
server.setRoute('/hello', (req: any, res: any) => {
|
server.setRoute('/hello', (req: any, res: any) => {
|
||||||
res.write('served via server');
|
res.write('served via server');
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"vue-router": "^4.1.5"
|
"vue-router": "^4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^4.1.0",
|
"@vitejs/plugin-vue": "^5.2.0",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"vite": "^5.2.8",
|
"vite": "^5.2.8",
|
||||||
|
|
|
||||||
|
|
@ -104,3 +104,25 @@ it('should not stall on evaluate when dismissing beforeunload', async ({ page, s
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not stall on click when dismissing beforeunload', async ({ page, server }) => {
|
||||||
|
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33806' });
|
||||||
|
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
await page.setContent(`<a href="${server.PREFIX}/frames/one-frame.html">click me</a>`);
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.onbeforeunload = () => false;
|
||||||
|
});
|
||||||
|
page.on('dialog', async dialog => {
|
||||||
|
await dialog.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByRole('link').click({ noWaitAfter: true });
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.onbeforeunload = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// This line should not timeout.
|
||||||
|
await page.getByRole('link').click({ timeout: 5000 });
|
||||||
|
await expect(page).toHaveURL(server.PREFIX + '/frames/one-frame.html');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -436,6 +436,7 @@ it('should not auto play audio', {
|
||||||
}
|
}
|
||||||
}, async ({ page, browserName, isWindows }) => {
|
}, async ({ page, browserName, isWindows }) => {
|
||||||
it.fixme(browserName === 'webkit' && isWindows);
|
it.fixme(browserName === 'webkit' && isWindows);
|
||||||
|
it.skip(process.env.PW_CLOCK === 'frozen', 'no way to inject real setTimeout');
|
||||||
await page.route('**/*', async route => {
|
await page.route('**/*', async route => {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|
|
||||||
|
|
@ -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());
|
const predicate = (msg: ConsoleMessage) => msg.type() === 'error' && /Content[\- ]Security[\- ]Policy/i.test(msg.text());
|
||||||
await expect(page.waitForEvent('console', { predicate, timeout: 1000 })).rejects.toThrow();
|
await expect(page.waitForEvent('console', { predicate, timeout: 1000 })).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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' })`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,8 @@
|
||||||
import { browserTest as it, expect } from '../config/browserTest';
|
import { browserTest as it, expect } from '../config/browserTest';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
it('should be able to save file', async ({ contextFactory, headless, browserName }, testInfo) => {
|
it('should be able to save file', async ({ contextFactory, browserName }, testInfo) => {
|
||||||
it.skip(!headless || browserName !== 'chromium', 'Printing to pdf is currently only supported in headless chromium.');
|
it.skip(browserName !== 'chromium', 'Printing to pdf is currently only supported in chromium.');
|
||||||
|
|
||||||
const context = await contextFactory();
|
const context = await contextFactory();
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
const outputFile = testInfo.outputPath('output.pdf');
|
const outputFile = testInfo.outputPath('output.pdf');
|
||||||
|
|
@ -27,9 +26,8 @@ it('should be able to save file', async ({ contextFactory, headless, browserName
|
||||||
expect(fs.readFileSync(outputFile).byteLength).toBeGreaterThan(0);
|
expect(fs.readFileSync(outputFile).byteLength).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to generate outline', async ({ contextFactory, server, headless, browserName }, testInfo) => {
|
it('should be able to generate outline', async ({ contextFactory, server, browserName }, testInfo) => {
|
||||||
it.skip(!headless || browserName !== 'chromium', 'Printing to pdf is currently only supported in headless chromium.');
|
it.skip(browserName !== 'chromium', 'Printing to pdf is currently only supported in chromium.');
|
||||||
// const context = await contextFactory();
|
|
||||||
const context = await contextFactory({
|
const context = await contextFactory({
|
||||||
baseURL: server.PREFIX,
|
baseURL: server.PREFIX,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -309,7 +309,7 @@ it.describe('selector generator', () => {
|
||||||
expect(await generate(page, 'input[mark="1"]')).toBe('internal:role=textbox >> nth=1');
|
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 }) => {
|
it('role', async ({ page }) => {
|
||||||
await page.setContent(`<input name="foobar" type="text"/>`);
|
await page.setContent(`<input name="foobar" type="text"/>`);
|
||||||
expect(await generate(page, 'input')).toBe('internal:role=textbox');
|
expect(await generate(page, 'input')).toBe('internal:role=textbox');
|
||||||
|
|
@ -596,4 +596,23 @@ it.describe('selector generator', () => {
|
||||||
`span >> nth=1`,
|
`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`,
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
it('should respect aria-owns', async ({ page }) => {
|
||||||
await page.setContent(`
|
await page.setContent(`
|
||||||
<a href='about:blank' aria-owns='input p'>
|
<a href='about:blank' aria-owns='input p'>
|
||||||
|
|
@ -508,3 +520,57 @@ it('should handle long strings', async ({ page }) => {
|
||||||
- region: ${s}
|
- region: ${s}
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should escape special yaml characters', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<a href="#">@hello</a>@hello
|
||||||
|
<a href="#">]hello</a>]hello
|
||||||
|
<a href="#">hello\n</a>
|
||||||
|
hello\n<a href="#">\n hello</a>\n hello
|
||||||
|
<a href="#">#hello</a>#hello
|
||||||
|
`);
|
||||||
|
|
||||||
|
await checkAndMatchSnapshot(page.locator('body'), `
|
||||||
|
- link "@hello"
|
||||||
|
- text: "@hello"
|
||||||
|
- link "]hello"
|
||||||
|
- text: "]hello"
|
||||||
|
- link "hello"
|
||||||
|
- text: hello
|
||||||
|
- link "hello"
|
||||||
|
- text: hello
|
||||||
|
- link "#hello"
|
||||||
|
- text: "#hello"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should escape special yaml values', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<a href="#">true</a>False
|
||||||
|
<a href="#">NO</a>yes
|
||||||
|
<a href="#">y</a>N
|
||||||
|
<a href="#">on</a>Off
|
||||||
|
<a href="#">null</a>NULL
|
||||||
|
<a href="#">123</a>123
|
||||||
|
<a href="#">-1.2</a>-1.2
|
||||||
|
<input type=text value="555">
|
||||||
|
`);
|
||||||
|
|
||||||
|
await checkAndMatchSnapshot(page.locator('body'), `
|
||||||
|
- link "true"
|
||||||
|
- text: "False"
|
||||||
|
- link "NO"
|
||||||
|
- text: "yes"
|
||||||
|
- link "y"
|
||||||
|
- text: "N"
|
||||||
|
- link "on"
|
||||||
|
- text: "Off"
|
||||||
|
- link "null"
|
||||||
|
- text: "NULL"
|
||||||
|
- link "123"
|
||||||
|
- text: "123"
|
||||||
|
- link "-1.2"
|
||||||
|
- text: "-1.2"
|
||||||
|
- textbox: "555"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
|
||||||
172
tests/playwright-test/aria-snapshot-file.spec.ts
Normal file
172
tests/playwright-test/aria-snapshot-file.spec.ts
Normal 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]');
|
||||||
|
});
|
||||||
|
|
@ -115,6 +115,71 @@ test('should check types of fixtures', async ({ runTSC }) => {
|
||||||
await use(x);
|
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': `
|
'playwright.config.ts': `
|
||||||
import { MyOptions } from './helper';
|
import { MyOptions } from './helper';
|
||||||
|
|
|
||||||
|
|
@ -339,3 +339,27 @@ test('should show request source context id', async ({ runUITest, server }) => {
|
||||||
await expect(page.getByText('page#2')).toBeVisible();
|
await expect(page.getByText('page#2')).toBeVisible();
|
||||||
await expect(page.getByText('api#1')).toBeVisible();
|
await expect(page.getByText('api#1')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should filter actions tab on double-click', async ({ runUITest, server }) => {
|
||||||
|
const { page } = await runUITest({
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('pass', async ({ page }) => {
|
||||||
|
await page.goto('${server.EMPTY_PAGE}');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByText('pass').dblclick();
|
||||||
|
|
||||||
|
const actionsTree = page.getByTestId('actions-tree');
|
||||||
|
await expect(actionsTree.getByRole('treeitem')).toHaveText([
|
||||||
|
/Before Hooks/,
|
||||||
|
/page.goto/,
|
||||||
|
/After Hooks/,
|
||||||
|
]);
|
||||||
|
await actionsTree.getByRole('treeitem', { name: 'page.goto' }).dblclick();
|
||||||
|
await expect(actionsTree.getByRole('treeitem')).toHaveText([
|
||||||
|
/page.goto/,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,55 @@ test('should update missing snapshots', async ({ runInlineTest }, testInfo) => {
|
||||||
expect(result2.exitCode).toBe(0);
|
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) => {
|
test('should generate baseline with regex', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'.git/marker': '',
|
'.git/marker': '',
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ set -x
|
||||||
|
|
||||||
trap "cd $(pwd -P)" EXIT
|
trap "cd $(pwd -P)" EXIT
|
||||||
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
|
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
|
||||||
NODE_VERSION="22.11.0" # autogenerated via ./update-playwright-driver-version.mjs
|
NODE_VERSION="22.12.0" # autogenerated via ./update-playwright-driver-version.mjs
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
PACKAGE_VERSION=$(node -p "require('../../package.json').version")
|
PACKAGE_VERSION=$(node -p "require('../../package.json').version")
|
||||||
|
|
|
||||||
23
utils/generate_types/overrides-test.d.ts
vendored
23
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -78,7 +78,7 @@ export type TestDetails = {
|
||||||
type TestBody<TestArgs> = (args: TestArgs, testInfo: TestInfo) => Promise<void> | void;
|
type TestBody<TestArgs> = (args: TestArgs, testInfo: TestInfo) => Promise<void> | void;
|
||||||
type ConditionBody<TestArgs> = (args: TestArgs) => boolean;
|
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, body: TestBody<TestArgs & WorkerArgs>): void;
|
||||||
(title: string, details: TestDetails, 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;
|
use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void;
|
||||||
step<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>;
|
step<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>;
|
||||||
expect: Expect<{}>;
|
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;
|
info(): TestInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyValue = { [key: string]: any };
|
export type TestFixture<R, Args extends {}> = (args: Args, use: (r: R) => Promise<void>, testInfo: TestInfo) => any;
|
||||||
export type TestFixture<R, Args extends KeyValue> = (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;
|
||||||
export type WorkerFixture<R, Args extends KeyValue> = (args: Args, use: (r: R) => Promise<void>, workerInfo: WorkerInfo) => any;
|
type TestFixtureValue<R, Args extends {}> = Exclude<R, Function> | TestFixture<R, Args>;
|
||||||
type TestFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | TestFixture<R, Args>;
|
type WorkerFixtureValue<R, Args extends {}> = Exclude<R, Function> | WorkerFixture<R, Args>;
|
||||||
type WorkerFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | WorkerFixture<R, Args>;
|
export type Fixtures<T extends {} = {}, W extends {} = {}, PT extends {} = {}, PW extends {} = {}> = {
|
||||||
export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extends KeyValue = {}, PW extends KeyValue = {}> = {
|
|
||||||
[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 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 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';
|
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>(config: PlaywrightTestConfig<T>): PlaywrightTestConfig<T>;
|
||||||
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>): PlaywrightTestConfig<T, W>;
|
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>): PlaywrightTestConfig<T, W>;
|
||||||
export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig;
|
export function defineConfig(config: PlaywrightTestConfig, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig;
|
||||||
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T>;
|
export function defineConfig<T>(config: PlaywrightTestConfig<T>, ...configs: PlaywrightTestConfig<T>[]): PlaywrightTestConfig<T>;
|
||||||
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>, ...configs: PlaywrightTestConfig[]): PlaywrightTestConfig<T, W>;
|
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 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> : {};
|
type MergedW<List> = List extends [TestType<any, infer W>, ...(infer Rest)] ? W & MergedW<Rest> : {};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue