Merge branch 'main' of https://github.com/JacksonLei123/playwright
This commit is contained in:
commit
1d953fab7b
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"name": "Playwright",
|
||||
"image": "mcr.microsoft.com/playwright:next",
|
||||
"postCreateCommand": "npm install && npm run build && apt-get update && apt-get install -y software-properties-common && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - && add-apt-repository \"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" && apt-get install -y docker-ce-cli",
|
||||
"settings": {
|
||||
"terminal.integrated.shell.linux": "/bin/bash"
|
||||
},
|
||||
"runArgs": [
|
||||
"-v", "/var/run/docker.sock:/var/run/docker.sock"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# 🎭 Playwright
|
||||
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
|
||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|||
|
||||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->132.0.6834.6<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Chromium <!-- GEN:chromium-version -->132.0.6834.15<!-- 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: |
|
||||
|
||||
|
|
|
|||
|
|
@ -93,11 +93,20 @@ Element is considered stable when it has maintained the same bounding box for at
|
|||
|
||||
## Enabled
|
||||
|
||||
Element is considered enabled unless it is a `<button>`, `<select>`, `<input>` or `<textarea>` with a `disabled` property.
|
||||
Element is considered enabled when it is **not disabled**.
|
||||
|
||||
Element is **disabled** when:
|
||||
- it is a `<button>`, `<select>`, `<input>`, `<textarea>`, `<option>` or `<optgroup>` with a `[disabled]` attribute;
|
||||
- it is a `<button>`, `<select>`, `<input>`, `<textarea>`, `<option>` or `<optgroup>` that is a part of a `<fieldset>` with a `[disabled]` attribute;
|
||||
- it is a descendant of an element with `[aria-disabled=true]` attribute.
|
||||
|
||||
## Editable
|
||||
|
||||
Element is considered editable when it is [enabled] and does not have `readonly` property set.
|
||||
Element is considered editable when it is [enabled] and is **not readonly**.
|
||||
|
||||
Element is **readonly** when:
|
||||
- it is a `<select>`, `<input>` or `<textarea>` with a `[readonly]` attribute;
|
||||
- it has an `[aria-readonly=true]` attribute and an aria role that [supports it](https://w3c.github.io/aria/#aria-readonly).
|
||||
|
||||
## Receives Events
|
||||
|
||||
|
|
|
|||
|
|
@ -1483,7 +1483,7 @@ Boolean disabled = await page.GetByRole(AriaRole.Button).IsDisabledAsync();
|
|||
* since: v1.14
|
||||
- returns: <[boolean]>
|
||||
|
||||
Returns whether the element is [editable](../actionability.md#editable).
|
||||
Returns whether the element is [editable](../actionability.md#editable). If the target element is not an `<input>`, `<textarea>`, `<select>`, `[contenteditable]` and does not have a role allowing `[aria-readonly]`, this method throws an error.
|
||||
|
||||
:::warning[Asserting editable state]
|
||||
If you need to assert that an element is editable, prefer [`method: LocatorAssertions.toBeEditable`] to avoid flakiness. See [assertions guide](../test-assertions.md) for more details.
|
||||
|
|
|
|||
|
|
@ -442,6 +442,23 @@ Expected options currently selected.
|
|||
### option: LocatorAssertions.NotToHaveValues.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.23
|
||||
|
||||
## async method: LocatorAssertions.NotToMatchAriaSnapshot
|
||||
* since: v1.49
|
||||
* langs: python
|
||||
|
||||
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot`].
|
||||
|
||||
### param: LocatorAssertions.NotToMatchAriaSnapshot.expected
|
||||
* since: v1.49
|
||||
- `expected` <string>
|
||||
|
||||
### option: LocatorAssertions.NotToMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
|
||||
* since: v1.49
|
||||
|
||||
### option: LocatorAssertions.NotToMatchAriaSnapshot.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.49
|
||||
|
||||
|
||||
|
||||
## async method: LocatorAssertions.toBeAttached
|
||||
* since: v1.33
|
||||
|
|
@ -2122,7 +2139,7 @@ await expect(page.locator('body')).toMatchAriaSnapshot(`
|
|||
```
|
||||
|
||||
```python async
|
||||
await page.goto('https://demo.playwright.dev/todomvc/')
|
||||
await page.goto("https://demo.playwright.dev/todomvc/")
|
||||
await expect(page.locator('body')).to_match_aria_snapshot('''
|
||||
- heading "todos"
|
||||
- textbox "What needs to be done?"
|
||||
|
|
@ -2130,7 +2147,7 @@ await expect(page.locator('body')).to_match_aria_snapshot('''
|
|||
```
|
||||
|
||||
```python sync
|
||||
page.goto('https://demo.playwright.dev/todomvc/')
|
||||
page.goto("https://demo.playwright.dev/todomvc/")
|
||||
expect(page.locator('body')).to_match_aria_snapshot('''
|
||||
- heading "todos"
|
||||
- textbox "What needs to be done?"
|
||||
|
|
|
|||
|
|
@ -302,10 +302,10 @@ await test.step('Log in', async () => {
|
|||
```java
|
||||
// All actions between group and groupEnd
|
||||
// will be shown in the trace viewer as a group.
|
||||
page.context().tracing.group("Open Playwright.dev > API");
|
||||
page.context().tracing().group("Open Playwright.dev > API");
|
||||
page.navigate("https://playwright.dev/");
|
||||
page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("API")).click();
|
||||
page.context().tracing.groupEnd();
|
||||
page.context().tracing().groupEnd();
|
||||
```
|
||||
|
||||
```python sync
|
||||
|
|
@ -329,10 +329,10 @@ await page.context.tracing.group_end()
|
|||
```csharp
|
||||
// All actions between GroupAsync and GroupEndAsync
|
||||
// will be shown in the trace viewer as a group.
|
||||
await Page.Context().Tracing.GroupAsync("Open Playwright.dev > API");
|
||||
await Page.Context.Tracing.GroupAsync("Open Playwright.dev > API");
|
||||
await Page.GotoAsync("https://playwright.dev/");
|
||||
await Page.GetByRole(AriaRole.Link, new() { Name = "API" }).ClickAsync();
|
||||
await Page.Context().Tracing.GroupEndAsync();
|
||||
await Page.Context.Tracing.GroupEndAsync();
|
||||
```
|
||||
|
||||
### param: Tracing.group.name
|
||||
|
|
|
|||
|
|
@ -1,36 +1,142 @@
|
|||
---
|
||||
id: aria-snapshots
|
||||
title: "Aria snapshots"
|
||||
title: "Snapshot testing"
|
||||
---
|
||||
import LiteYouTube from '@site/src/components/LiteYouTube';
|
||||
|
||||
## Overview
|
||||
|
||||
In Playwright, aria snapshots provide a YAML representation of the accessibility tree of a page.
|
||||
These snapshots can be stored and compared later to verify if the page structure remains consistent or meets defined
|
||||
expectations.
|
||||
With the Playwright Snapshot testing you can assert the accessibility tree of a page against a predefined snapshot template.
|
||||
|
||||
```js
|
||||
await page.goto('https://playwright.dev/');
|
||||
await expect(page.getByRole('banner')).toMatchAriaSnapshot(`
|
||||
- banner:
|
||||
- heading /Playwright enables reliable end-to-end/ [level=1]
|
||||
- link "Get started"
|
||||
- link "Star microsoft/playwright on GitHub"
|
||||
- link /[\\d]+k\\+ stargazers on GitHub/
|
||||
`);
|
||||
```
|
||||
|
||||
```python sync
|
||||
page.goto('https://playwright.dev/')
|
||||
expect(page.query_selector('banner')).to_match_aria_snapshot("""
|
||||
- banner:
|
||||
- heading /Playwright enables reliable end-to-end/ [level=1]
|
||||
- link "Get started"
|
||||
- link "Star microsoft/playwright on GitHub"
|
||||
- link /[\\d]+k\\+ stargazers on GitHub/
|
||||
""")
|
||||
```
|
||||
|
||||
```python async
|
||||
await page.goto('https://playwright.dev/')
|
||||
await expect(page.query_selector('banner')).to_match_aria_snapshot("""
|
||||
- banner:
|
||||
- heading /Playwright enables reliable end-to-end/ [level=1]
|
||||
- link "Get started"
|
||||
- link "Star microsoft/playwright on GitHub"
|
||||
- link /[\\d]+k\\+ stargazers on GitHub/
|
||||
""")
|
||||
```
|
||||
|
||||
```java
|
||||
page.navigate("https://playwright.dev/");
|
||||
assertThat(page.locator("banner")).matchesAriaSnapshot("""
|
||||
- banner:
|
||||
- heading /Playwright enables reliable end-to-end/ [level=1]
|
||||
- link "Get started"
|
||||
- link "Star microsoft/playwright on GitHub"
|
||||
- link /[\\d]+k\\+ stargazers on GitHub/
|
||||
""");
|
||||
```
|
||||
|
||||
```csharp
|
||||
await page.GotoAsync("https://playwright.dev/");
|
||||
await Expect(page.Locator("banner")).ToMatchAriaSnapshotAsync(@"
|
||||
- banner:
|
||||
- heading ""Playwright enables reliable end-to-end testing for modern web apps."" [level=1]
|
||||
- link ""Get started""
|
||||
- link ""Star microsoft/playwright on GitHub""
|
||||
- link /[\\d]+k\\+ stargazers on GitHub/
|
||||
");
|
||||
```
|
||||
|
||||
<LiteYouTube
|
||||
id="P4R6hnsE0UY"
|
||||
title="Getting started with ARIA Snapshots"
|
||||
/>
|
||||
|
||||
## Assertion testing vs Snapshot testing
|
||||
|
||||
Snapshot testing and assertion testing serve different purposes in test automation:
|
||||
|
||||
### Assertion testing
|
||||
Assertion testing is a targeted approach where you assert specific values or conditions about elements or components. For instance, with Playwright, [`method: LocatorAssertions.toHaveText`]
|
||||
verifies that an element contains the expected text, and [`method: LocatorAssertions.toHaveValue`]
|
||||
confirms that an input field has the expected value.
|
||||
Assertion tests are specific and generally check the current state of an element or property
|
||||
against an expected, predefined state.
|
||||
They work well for predictable, single-value checks but are limited in scope when testing the
|
||||
broader structure or variations.
|
||||
|
||||
**Advantages**
|
||||
- **Clarity**: The intent of the test is explicit and easy to understand.
|
||||
- **Specificity**: Tests focus on particular aspects of functionality, making them more robust
|
||||
against unrelated changes.
|
||||
- **Debugging**: Failures provide targeted feedback, pointing directly to the problematic aspect.
|
||||
|
||||
**Disadvantages**
|
||||
- **Verbose for complex outputs**: Writing assertions for complex data structures or large outputs
|
||||
can be cumbersome and error-prone.
|
||||
- **Maintenance overhead**: As code evolves, manually updating assertions can be time-consuming.
|
||||
|
||||
### Snapshot testing
|
||||
Snapshot testing captures a “snapshot” or representation of the entire
|
||||
state of an element, component, or data at a given moment, which is then saved for future
|
||||
comparisons. When re-running tests, the current state is compared to the snapshot, and if there
|
||||
are differences, the test fails. This approach is especially useful for complex or dynamic
|
||||
structures, where manually asserting each detail would be too time-consuming. Snapshot testing
|
||||
is broader and more holistic than assertion testing, allowing you to track more complex changes over time.
|
||||
|
||||
**Advantages**
|
||||
- **Simplifies complex outputs**: For example, testing a UI component's rendered output can be tedious with traditional assertions. Snapshots capture the entire output for easy comparison.
|
||||
- **Quick Feedback loop**: Developers can easily spot unintended changes in the output.
|
||||
- **Encourages consistency**: Helps maintain consistent output as code evolves.
|
||||
|
||||
**Disadvantages**
|
||||
- **Over-Reliance**: It can be tempting to accept changes to snapshots without fully understanding
|
||||
them, potentially hiding bugs.
|
||||
- **Granularity**: Large snapshots may be hard to interpret when differences arise, especially
|
||||
if minor changes affect large portions of the output.
|
||||
- **Suitability**: Not ideal for highly dynamic content where outputs change frequently or
|
||||
unpredictably.
|
||||
|
||||
### When to use
|
||||
|
||||
- **Snapshot testing** is ideal for:
|
||||
- UI testing of whole pages and components.
|
||||
- Broad structural checks for complex UI components.
|
||||
- Regression testing for outputs that rarely change structure.
|
||||
|
||||
- **Assertion testing** is ideal for:
|
||||
- Core logic validation.
|
||||
- Computed value testing.
|
||||
- Fine-grained tests requiring precise conditions.
|
||||
|
||||
By combining snapshot testing for broad, structural checks and assertion testing for specific functionality, you can achieve a well-rounded testing strategy.
|
||||
|
||||
## Aria snapshots
|
||||
|
||||
In Playwright, aria snapshots provide a YAML representation of the accessibility tree of a page.
|
||||
These snapshots can be stored and compared later to verify if the page structure remains consistent or meets defined
|
||||
expectations.
|
||||
|
||||
The YAML format describes the hierarchical structure of accessible elements on the page, detailing **roles**, **attributes**, **values**, and **text content**.
|
||||
The structure follows a tree-like syntax, where each node represents an accessible element, and indentation indicates
|
||||
nested elements.
|
||||
|
||||
Following is a simple example of an aria snapshot for the playwright.dev homepage:
|
||||
|
||||
```yaml
|
||||
- banner:
|
||||
- heading /Playwright enables reliable/ [level=1]
|
||||
- link "Get started"
|
||||
- link "Star microsoft/playwright on GitHub"
|
||||
- main:
|
||||
- img "Browsers (Chromium, Firefox, WebKit)"
|
||||
- heading "Any browser • Any platform • One API"
|
||||
```
|
||||
|
||||
Each accessible element in the tree is represented as a YAML node:
|
||||
|
||||
```yaml
|
||||
|
|
@ -67,19 +173,19 @@ await expect(page.locator('body')).toMatchAriaSnapshot(`
|
|||
```
|
||||
|
||||
```python sync
|
||||
page.locator("body").to_match_aria_snapshot("""
|
||||
expect(page.locator("body")).to_match_aria_snapshot("""
|
||||
- heading "title"
|
||||
""")
|
||||
```
|
||||
|
||||
```python async
|
||||
await page.locator("body").to_match_aria_snapshot("""
|
||||
await expect(page.locator("body")).to_match_aria_snapshot("""
|
||||
- heading "title"
|
||||
""")
|
||||
```
|
||||
|
||||
```java
|
||||
page.locator("body").expect().toMatchAriaSnapshot("""
|
||||
assertThat(page.locator("body")).matchesAriaSnapshot("""
|
||||
- heading "title"
|
||||
""");
|
||||
```
|
||||
|
|
@ -185,7 +291,7 @@ interactive interface:
|
|||
- **"Assert snapshot" Action**: In the code generator, you can use the "Assert snapshot" action to automatically create
|
||||
a snapshot assertion for the selected elements. This is a quick way to capture the aria snapshot as part of your
|
||||
recorded test flow.
|
||||
|
||||
|
||||
- **"Aria snapshot" Tab**: The "Aria snapshot" tab within the code generator interface visually represents the
|
||||
aria snapshot for a selected locator, letting you explore, inspect, and verify element roles, attributes, and
|
||||
accessible names to aid snapshot creation and review.
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ Browser builds for Firefox and WebKit are built for the [glibc](https://en.wikip
|
|||
You can use the [.NET install script](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script) in order to install different SDK versions:
|
||||
|
||||
```bash
|
||||
curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --install-dir /usr/share/dotnet --channel 6.0
|
||||
curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --install-dir /usr/share/dotnet --channel 9.0
|
||||
```
|
||||
|
||||
## Build your own image
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ title: "Installation"
|
|||
|
||||
Playwright was created specifically to accommodate the needs of end-to-end testing. Playwright supports all modern rendering engines including Chromium, WebKit, and Firefox. Test on Windows, Linux, and macOS, locally or on CI, headless or headed with native mobile emulation.
|
||||
|
||||
You can choose to use [MSTest base classes](./test-runners.md#mstest) or [NUnit base classes](./test-runners.md#nunit) that Playwright provides to write end-to-end tests. These classes support running tests on multiple browser engines, parallelizing tests, adjusting launch/context options and getting a [Page]/[BrowserContext] instance per test out of the box. Alternatively you can use the [library](./library.md) to manually write the testing infrastructure.
|
||||
You can choose to use [MSTest base classes](./test-runners.md) or [NUnit base classes](./test-runners.md) that Playwright provides to write end-to-end tests. These classes support running tests on multiple browser engines, parallelizing tests, adjusting launch/context options and getting a [Page]/[BrowserContext] instance per test out of the box. Alternatively you can use the [library](./library.md) to manually write the testing infrastructure.
|
||||
|
||||
1. Start by creating a new project with `dotnet new`. This will create the `PlaywrightTests` directory which includes a `UnitTest1.cs` file:
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ You can choose any testing framework such as JUnit or TestNG based on your proje
|
|||
|
||||
## .NET
|
||||
|
||||
Playwright for .NET comes with [MSTest base classes](https://playwright.dev/dotnet/docs/test-runners#mstest) and [NUnit base classes](https://playwright.dev/dotnet/docs/test-runners#nunit) for writing end-to-end tests.
|
||||
Playwright for .NET comes with [MSTest base classes](https://playwright.dev/dotnet/docs/test-runners) and [NUnit base classes](https://playwright.dev/dotnet/docs/test-runners) for writing end-to-end tests.
|
||||
|
||||
* [Documentation](https://playwright.dev/dotnet/docs/intro)
|
||||
* [GitHub repo](https://github.com/microsoft/playwright-dotnet)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ title: "Getting started - Library"
|
|||
|
||||
## Introduction
|
||||
|
||||
Playwright can either be used with the [MSTest](./test-runners.md#mstest) or [NUnit](./test-runners.md#nunit), or as a Playwright Library (this guide). If you are working on an application that utilizes Playwright capabilities or you are using Playwright with another test runner, read on.
|
||||
Playwright can either be used with the [MSTest](./test-runners.md) or [NUnit](./test-runners.md), or as a Playwright Library (this guide). If you are working on an application that utilizes Playwright capabilities or you are using Playwright with another test runner, read on.
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,91 @@ toc_max_heading_level: 2
|
|||
---
|
||||
|
||||
|
||||
## Version 1.49
|
||||
|
||||
### Aria snapshots
|
||||
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
|
||||
```csharp
|
||||
await page.GotoAsync("https://playwright.dev");
|
||||
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(@"
|
||||
- banner:
|
||||
- heading /Playwright enables reliable/ [level=1]
|
||||
- link ""Get started""
|
||||
- link ""Star microsoft/playwright on GitHub""
|
||||
- main:
|
||||
- img ""Browsers (Chromium, Firefox, WebKit)""
|
||||
- heading ""Any browser • Any platform • One API""
|
||||
");
|
||||
```
|
||||
|
||||
You can generate this assertion with [Test Generator](./codegen) or by calling [`method: Locator.ariaSnapshot`].
|
||||
|
||||
Learn more in the [aria snapshots guide](./aria-snapshots).
|
||||
|
||||
### Tracing groups
|
||||
|
||||
New method [`method: Tracing.group`] allows you to visually group actions in the trace viewer.
|
||||
|
||||
```csharp
|
||||
// All actions between GroupAsync and GroupEndAsync
|
||||
// will be shown in the trace viewer as a group.
|
||||
await Page.Context.Tracing.GroupAsync("Open Playwright.dev > API");
|
||||
await Page.GotoAsync("https://playwright.dev/");
|
||||
await Page.GetByRole(AriaRole.Link, new() { Name = "API" }).ClickAsync();
|
||||
await Page.Context.Tracing.GroupEndAsync();
|
||||
```
|
||||
|
||||
### Breaking: `chrome` and `msedge` channels switch to new headless mode
|
||||
|
||||
This change affects you if you're using one of the following channels in your `playwright.config.ts`:
|
||||
- `chrome`, `chrome-dev`, `chrome-beta`, or `chrome-canary`
|
||||
- `msedge`, `msedge-dev`, `msedge-beta`, or `msedge-canary`
|
||||
|
||||
After updating to Playwright v1.49, run your test suite. If it still passes, you're good to go. If not, you will probably need to update your snapshots, and adapt some of your test code around PDF viewers and extensions. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for more details.
|
||||
|
||||
### Try new Chromium headless
|
||||
|
||||
You can opt into the new headless mode by using `'chromium'` channel. As [official Chrome documentation puts it](https://developer.chrome.com/blog/chrome-headless-shell):
|
||||
|
||||
> New Headless on the other hand is the real Chrome browser, and is thus more authentic, reliable, and offers more features. This makes it more suitable for high-accuracy end-to-end web app testing or browser extension testing.
|
||||
|
||||
See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for the list of possible breakages you could encounter and more details on Chromium headless. Please file an issue if you see any problems after opting in.
|
||||
|
||||
```xml csharp title="runsettings.xml"
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RunSettings>
|
||||
<Playwright>
|
||||
<BrowserName>chromium</BrowserName>
|
||||
<LaunchOptions>
|
||||
<Channel>chromium</Channel>
|
||||
</LaunchOptions>
|
||||
</Playwright>
|
||||
</RunSettings>
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
dotnet test -- Playwright.BrowserName=chromium Playwright.LaunchOptions.Channel=chromium
|
||||
```
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- There will be no more updates for WebKit on Ubuntu 20.04 and Debian 11. We recommend updating your OS to a later version.
|
||||
- `<canvas>` elements inside a snapshot now draw a preview.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
- Chromium 131.0.6778.33
|
||||
- Mozilla Firefox 132.0
|
||||
- WebKit 18.2
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
- Google Chrome 130
|
||||
- Microsoft Edge 130
|
||||
|
||||
|
||||
## Version 1.48
|
||||
|
||||
### WebSocket routing
|
||||
|
|
|
|||
|
|
@ -4,6 +4,79 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.49
|
||||
|
||||
### Aria snapshots
|
||||
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
|
||||
```java
|
||||
page.navigate("https://playwright.dev");
|
||||
assertThat(page.locator("body")).matchesAriaSnapshot("""
|
||||
- banner:
|
||||
- heading /Playwright enables reliable/ [level=1]
|
||||
- link "Get started"
|
||||
- link "Star microsoft/playwright on GitHub"
|
||||
- main:
|
||||
- img "Browsers (Chromium, Firefox, WebKit)"
|
||||
- heading "Any browser • Any platform • One API"
|
||||
""");
|
||||
```
|
||||
|
||||
You can generate this assertion with [Test Generator](./codegen) or by calling [`method: Locator.ariaSnapshot`].
|
||||
|
||||
Learn more in the [aria snapshots guide](./aria-snapshots).
|
||||
|
||||
### Tracing groups
|
||||
|
||||
New method [`method: Tracing.group`] allows you to visually group actions in the trace viewer.
|
||||
|
||||
```java
|
||||
// All actions between group and groupEnd
|
||||
// will be shown in the trace viewer as a group.
|
||||
page.context().tracing().group("Open Playwright.dev > API");
|
||||
page.navigate("https://playwright.dev/");
|
||||
page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("API")).click();
|
||||
page.context().tracing().groupEnd();
|
||||
```
|
||||
|
||||
### Breaking: `chrome` and `msedge` channels switch to new headless mode
|
||||
|
||||
This change affects you if you're using one of the following channels in your `playwright.config.ts`:
|
||||
- `chrome`, `chrome-dev`, `chrome-beta`, or `chrome-canary`
|
||||
- `msedge`, `msedge-dev`, `msedge-beta`, or `msedge-canary`
|
||||
|
||||
After updating to Playwright v1.49, run your test suite. If it still passes, you're good to go. If not, you will probably need to update your snapshots, and adapt some of your test code around PDF viewers and extensions. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for more details.
|
||||
|
||||
### Try new Chromium headless
|
||||
|
||||
You can opt into the new headless mode by using `'chromium'` channel. As [official Chrome documentation puts it](https://developer.chrome.com/blog/chrome-headless-shell):
|
||||
|
||||
> New Headless on the other hand is the real Chrome browser, and is thus more authentic, reliable, and offers more features. This makes it more suitable for high-accuracy end-to-end web app testing or browser extension testing.
|
||||
|
||||
See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for the list of possible breakages you could encounter and more details on Chromium headless. Please file an issue if you see any problems after opting in.
|
||||
|
||||
```java
|
||||
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setChannel("chromium"));
|
||||
```
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- There will be no more updates for WebKit on Ubuntu 20.04 and Debian 11. We recommend updating your OS to a later version.
|
||||
- `<canvas>` elements inside a snapshot now draw a preview.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
- Chromium 131.0.6778.33
|
||||
- Mozilla Firefox 132.0
|
||||
- WebKit 18.2
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
- Google Chrome 130
|
||||
- Microsoft Edge 130
|
||||
|
||||
|
||||
## Version 1.48
|
||||
|
||||
### WebSocket routing
|
||||
|
|
|
|||
|
|
@ -4,6 +4,80 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.49
|
||||
|
||||
### Aria snapshots
|
||||
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
|
||||
```python
|
||||
page.goto("https://playwright.dev")
|
||||
expect(page.locator('body')).to_match_aria_snapshot('''
|
||||
- banner:
|
||||
- heading /Playwright enables reliable/ [level=1]
|
||||
- link "Get started"
|
||||
- link "Star microsoft/playwright on GitHub"
|
||||
- main:
|
||||
- img "Browsers (Chromium, Firefox, WebKit)"
|
||||
- heading "Any browser • Any platform • One API"
|
||||
''')
|
||||
```
|
||||
|
||||
You can generate this assertion with [Test Generator](./codegen) or by calling [`method: Locator.ariaSnapshot`].
|
||||
|
||||
Learn more in the [aria snapshots guide](./aria-snapshots).
|
||||
|
||||
### Tracing groups
|
||||
|
||||
New method [`method: Tracing.group`] allows you to visually group actions in the trace viewer.
|
||||
|
||||
```python
|
||||
# All actions between group and group_end
|
||||
# will be shown in the trace viewer as a group.
|
||||
page.context.tracing.group("Open Playwright.dev > API")
|
||||
page.goto("https://playwright.dev/")
|
||||
page.get_by_role("link", name="API").click()
|
||||
page.context.tracing.group_end()
|
||||
```
|
||||
|
||||
### Breaking: `chrome` and `msedge` channels switch to new headless mode
|
||||
|
||||
This change affects you if you're using one of the following channels in your `playwright.config.ts`:
|
||||
- `chrome`, `chrome-dev`, `chrome-beta`, or `chrome-canary`
|
||||
- `msedge`, `msedge-dev`, `msedge-beta`, or `msedge-canary`
|
||||
|
||||
After updating to Playwright v1.49, run your test suite. If it still passes, you're good to go. If not, you will probably need to update your snapshots, and adapt some of your test code around PDF viewers and extensions. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for more details.
|
||||
|
||||
### Try new Chromium headless
|
||||
|
||||
You can opt into the new headless mode by using `'chromium'` channel. As [official Chrome documentation puts it](https://developer.chrome.com/blog/chrome-headless-shell):
|
||||
|
||||
> New Headless on the other hand is the real Chrome browser, and is thus more authentic, reliable, and offers more features. This makes it more suitable for high-accuracy end-to-end web app testing or browser extension testing.
|
||||
|
||||
See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for the list of possible breakages you could encounter and more details on Chromium headless. Please file an issue if you see any problems after opting in.
|
||||
|
||||
```bash python
|
||||
pytest test_login.py --browser-channel chromium
|
||||
```
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- There will be no more updates for WebKit on Ubuntu 20.04 and Debian 11. We recommend updating your OS to a later version.
|
||||
- `<canvas>` elements inside a snapshot now draw a preview.
|
||||
- Python 3.8 is not supported anymore.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
- Chromium 131.0.6778.33
|
||||
- Mozilla Firefox 132.0
|
||||
- WebKit 18.2
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
- Google Chrome 130
|
||||
- Microsoft Edge 130
|
||||
|
||||
|
||||
## Version 1.48
|
||||
|
||||
### WebSocket routing
|
||||
|
|
|
|||
|
|
@ -114,10 +114,16 @@ See [`property: TestConfig.shard`].
|
|||
|
||||
## property: FullConfig.updateSnapshots
|
||||
* since: v1.10
|
||||
- type: <[UpdateSnapshots]<"all"|"none"|"missing">>
|
||||
- type: <[UpdateSnapshots]<"all"|"changed"|"missing"|"none">>
|
||||
|
||||
See [`property: TestConfig.updateSnapshots`].
|
||||
|
||||
## property: FullConfig.updateSourceMethod
|
||||
* since: v1.50
|
||||
- type: <[UpdateSourceMethod]<"overwrite"|"3way"|"patch">>
|
||||
|
||||
See [`property: TestConfig.updateSourceMethod`].
|
||||
|
||||
## property: FullConfig.version
|
||||
* since: v1.20
|
||||
- type: <[string]>
|
||||
|
|
|
|||
|
|
@ -570,12 +570,13 @@ export default defineConfig({
|
|||
|
||||
## property: TestConfig.updateSnapshots
|
||||
* since: v1.10
|
||||
- type: ?<[UpdateSnapshots]<"all"|"none"|"missing">>
|
||||
- type: ?<[UpdateSnapshots]<"all"|"changed"|"missing"|"none">>
|
||||
|
||||
Whether to update expected snapshots with the actual results produced by the test run. Defaults to `'missing'`.
|
||||
* `'all'` - All tests that are executed will update snapshots that did not match. Matching snapshots will not be updated.
|
||||
* `'none'` - No snapshots are updated.
|
||||
* `'all'` - All tests that are executed will update snapshots.
|
||||
* `'changed'` - All tests that are executed will update snapshots that did not match. Matching snapshots will not be updated.
|
||||
* `'missing'` - Missing snapshots are created, for example when authoring a new test and running it for the first time. This is the default.
|
||||
* `'none'` - No snapshots are updated.
|
||||
|
||||
Learn more about [snapshots](../test-snapshots.md).
|
||||
|
||||
|
|
@ -589,6 +590,15 @@ export default defineConfig({
|
|||
});
|
||||
```
|
||||
|
||||
## property: TestConfig.updateSourceMethod
|
||||
* since: v1.50
|
||||
- type: ?<[UpdateSourceMethod]<"overwrite"|"3way"|"patch">>
|
||||
|
||||
Defines how to update the source code snapshots.
|
||||
* `'overwrite'` - Overwrite the source code snapshot with the actual result.
|
||||
* `'3way'` - Use a three-way merge to update the source code snapshot.
|
||||
* `'patch'` - Use a patch to update the source code snapshot. This is the default.
|
||||
|
||||
## property: TestConfig.use
|
||||
* since: v1.10
|
||||
- type: ?<[TestOptions]>
|
||||
|
|
|
|||
|
|
@ -76,33 +76,40 @@ Here are the most common options available in the command line.
|
|||
|
||||
Complete set of Playwright Test options is available in the [configuration file](./test-use-options.md). Following options can be passed to a command line and take priority over the configuration file:
|
||||
|
||||
<!-- // Note: packages/playwright/src/program.ts is the source of truth. -->
|
||||
|
||||
| Option | Description |
|
||||
| :- | :- |
|
||||
| Non-option arguments | Each argument is treated as a regular expression matched against the full test file path. Only tests from the files matching the pattern will be executed. Special symbols like `$` or `*` should be escaped with `\`. In many shells/terminals you may need to quote the arguments. |
|
||||
| `-c <file>` or `--config <file>`| Configuration file. If not passed, defaults to `playwright.config.ts` or `playwright.config.js` in the current directory. |
|
||||
| `--debug`| Run tests with Playwright Inspector. Shortcut for `PWDEBUG=1` environment variable and `--timeout=0 --max-failures=1 --headed --workers=1` options.|
|
||||
| `--fail-on-flaky-tests` | Fails test runs that contain flaky tests. By default flaky tests count as successes. |
|
||||
| `--forbid-only` | Whether to disallow `test.only`. Useful on CI.|
|
||||
| `--global-timeout <number>` | Total timeout for the whole test run in milliseconds. By default, there is no global timeout. Learn more about [various timeouts](./test-timeouts.md).|
|
||||
| `-g <grep>` or `--grep <grep>` | Only run tests matching this regular expression. For example, this will run `'should add to cart'` when passed `-g "add to cart"`. The regular expression will be tested against the string that consists of the project name, test file name, `test.describe` titles if any, test title and all test tags, separated by spaces, e.g. `chromium my-test.spec.ts my-suite my-test @smoke`. The filter does not apply to the tests from dependency projects, i.e. Playwright will still run all tests from [project dependencies](./test-projects.md#dependencies). |
|
||||
| `--grep-invert <grep>` | Only run tests **not** matching this regular expression. The opposite of `--grep`. The filter does not apply to the tests from dependency projects, i.e. Playwright will still run all tests from [project dependencies](./test-projects.md#dependencies).|
|
||||
| `--headed` | Run tests in headed browsers. Useful for debugging. |
|
||||
| `--ignore-snapshots` | Whether to ignore [snapshots](./test-snapshots.md). Use this when snapshot expectations are known to be different, e.g. running tests on Linux against Windows screenshots. |
|
||||
| `--last-failed` | Only re-run the failures.|
|
||||
| `--list` | list all the tests, but do not run them.|
|
||||
| `--max-failures <N>` or `-x`| Stop after the first `N` test failures. Passing `-x` stops after the first failure.|
|
||||
| `--no-deps` | Ignore the dependencies between projects and behave as if they were not specified. |
|
||||
| `--output <dir>` | Directory for artifacts produced by tests, defaults to `test-results`. |
|
||||
| `--only-changed [ref]` | Only run test files that have been changed between working tree and "ref". Defaults to running all uncommitted changes with ref=HEAD. Only supports Git. |
|
||||
| `--pass-with-no-tests` | Allows the test suite to pass when no files are found. |
|
||||
| `--project <name>` | Only run tests from the specified [projects](./test-projects.md), supports '*' wildcard. Defaults to running all projects defined in the configuration file.|
|
||||
| `--quiet` | Whether to suppress stdout and stderr from the tests. |
|
||||
| `--repeat-each <N>` | Run each test `N` times, defaults to one. |
|
||||
| `--reporter <reporter>` | Choose a reporter: minimalist `dot`, concise `line` or detailed `list`. See [reporters](./test-reporters.md) for more information. You can also pass a path to a [custom reporter](./test-reporters.md#custom-reporters) file. |
|
||||
| `--retries <number>` | The maximum number of [retries](./test-retries.md#retries) for flaky tests, defaults to zero (no retries). |
|
||||
| `--shard <shard>` | [Shard](./test-parallel.md#shard-tests-between-multiple-machines) tests and execute only selected shard, specified in the form `current/all`, 1-based, for example `3/5`.|
|
||||
| `--timeout <number>` | Maximum timeout in milliseconds for each test, defaults to 30 seconds. Learn more about [various timeouts](./test-timeouts.md).|
|
||||
| `--trace <mode>` | Force tracing mode, can be `on`, `off`, `on-first-retry`, `on-all-retries`, `retain-on-failure` |
|
||||
| `--tsconfig <path>` | Path to a single tsconfig applicable to all imported files. See [tsconfig resolution](./test-typescript.md#tsconfig-resolution) for more details. |
|
||||
| `--update-snapshots` or `-u` | Whether to update [snapshots](./test-snapshots.md) with actual results instead of comparing them. Use this when snapshot expectations have changed.|
|
||||
| `--workers <number>` or `-j <number>`| The maximum number of concurrent worker processes that run in [parallel](./test-parallel.md). |
|
||||
| Non-option arguments | Each argument is treated as a regular expression matched against the full test file path. Only tests from files matching the pattern will be executed. Special symbols like `$` or `*` should be escaped with `\`. In many shells/terminals you may need to quote the arguments. |
|
||||
| `-c <file>` or `--config <file>` | Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}". Defaults to `playwright.config.ts` or `playwright.config.js` in the current directory. |
|
||||
| `--debug` | Run tests with Playwright Inspector. Shortcut for `PWDEBUG=1` environment variable and `--timeout=0 --max-failures=1 --headed --workers=1` options. |
|
||||
| `--fail-on-flaky-tests` | Fail if any test is flagged as flaky (default: false). |
|
||||
| `--forbid-only` | Fail if `test.only` is called (default: false). Useful on CI. |
|
||||
| `--fully-parallel` | Run all tests in parallel (default: false). |
|
||||
| `--global-timeout <timeout>` | Maximum time this test suite can run in milliseconds (default: unlimited). |
|
||||
| `-g <grep>` or `--grep <grep>` | Only run tests matching this regular expression (default: ".*"). |
|
||||
| `-gv <grep>` or `--grep-invert <grep>` | Only run tests that do not match this regular expression. |
|
||||
| `--headed` | Run tests in headed browsers (default: headless). |
|
||||
| `--ignore-snapshots` | Ignore screenshot and snapshot expectations. |
|
||||
| `--last-failed` | Only re-run the failures. |
|
||||
| `--list` | Collect all the tests and report them, but do not run. |
|
||||
| `--max-failures <N>` or `-x` | Stop after the first `N` failures. Passing `-x` stops after the first failure. |
|
||||
| `--no-deps` | Do not run project dependencies. |
|
||||
| `--output <dir>` | Folder for output artifacts (default: "test-results"). |
|
||||
| `--only-changed [ref]` | Only run test files that have been changed between 'HEAD' and 'ref'. Defaults to running all uncommitted changes. Only supports Git. |
|
||||
| `--pass-with-no-tests` | Makes test run succeed even if no tests were found. |
|
||||
| `--project <project-name...>` | Only run tests from the specified list of projects, supports '*' wildcard (default: run all projects). |
|
||||
| `--quiet` | Suppress stdio. |
|
||||
| `--repeat-each <N>` | Run each test `N` times (default: 1). |
|
||||
| `--reporter <reporter>` | Reporter to use, comma-separated, can be "dot", "line", "list", or others (default: "list"). You can also pass a path to a custom reporter file. |
|
||||
| `--retries <retries>` | Maximum retry count for flaky tests, zero for no retries (default: no retries). |
|
||||
| `--shard <shard>` | Shard tests and execute only the selected shard, specified in the form "current/all", 1-based, e.g., "3/5". |
|
||||
| `--timeout <timeout>` | Specify test timeout threshold in milliseconds, zero for unlimited (default: 30 seconds). |
|
||||
| `--trace <mode>` | Force tracing mode, can be "on", "off", "on-first-retry", "on-all-retries", "retain-on-failure", "retain-on-first-failure". |
|
||||
| `--tsconfig <path>` | Path to a single tsconfig applicable to all imported files (default: look up tsconfig for each imported file separately). |
|
||||
| `--ui` | Run tests in interactive UI mode. |
|
||||
| `--ui-host <host>` | Host to serve UI on; specifying this option opens UI in a browser tab. |
|
||||
| `--ui-port <port>` | Port to serve UI on, 0 for any free port; specifying this option opens UI in a browser tab. |
|
||||
| `-u` or `--update-snapshots [mode]` | Update snapshots with actual results. Possible values are "all", "changed", "missing", and "none". Not passing defaults to "missing"; passing without a value defaults to "changed". |
|
||||
| `-j <workers>` or `--workers <workers>` | Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker (default: 50%). |
|
||||
| `-x` | Stop after the first failure. |
|
||||
|
|
|
|||
|
|
@ -5,19 +5,57 @@ title: "Test Runners"
|
|||
|
||||
## Introduction
|
||||
|
||||
While Playwright for .NET isn't tied to a particular test runner or testing framework, in our experience the easiest way of getting started is by using the base classes we provide for [MSTest](#mstest) and [NUnit](#nunit). These classes support running tests on multiple browser engines, adjusting launch/context options and getting a [Page]/[BrowserContext] instance per test out of the box.
|
||||
While Playwright for .NET isn't tied to a particular test runner or testing framework, in our experience the easiest way of getting started is by using the base classes we provide for MSTest and NUnit. These classes support running tests on multiple browser engines, adjusting launch/context options and getting a [Page]/[BrowserContext] instance per test out of the box.
|
||||
|
||||
Playwright and Browser instances will be reused between tests for better performance. We
|
||||
recommend running each test case in a new BrowserContext, this way browser state will be
|
||||
isolated between the tests.
|
||||
|
||||
## MSTest
|
||||
<Tabs
|
||||
groupId="test-runners"
|
||||
defaultValue="mstest"
|
||||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
||||
Playwright provides base classes to write tests with NUnit via the [`Microsoft.Playwright.NUnit`](https://www.nuget.org/packages/Microsoft.Playwright.NUnit) package.
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="mstest">
|
||||
|
||||
Playwright provides base classes to write tests with MSTest via the [`Microsoft.Playwright.MSTest`](https://www.nuget.org/packages/Microsoft.Playwright.MSTest) package.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Check out the [installation guide](./intro.md) to get started.
|
||||
|
||||
### Running MSTest tests in Parallel
|
||||
## Running tests in Parallel
|
||||
|
||||
<Tabs
|
||||
groupId="test-runners"
|
||||
defaultValue="mstest"
|
||||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
||||
By default NUnit will run all test files in parallel, while running tests inside each file sequentially (`ParallelScope.Self`). It will create as many processes as there are cores on the host system. You can adjust this behavior using the NUnit.NumberOfTestWorkers parameter.
|
||||
Only `ParallelScope.Self` is supported.
|
||||
|
||||
For CPU-bound tests, we recommend using as many workers as there are cores on your system, divided by 2. For IO-bound tests you can use as many workers as you have cores.
|
||||
|
||||
```bash
|
||||
dotnet test -- NUnit.NumberOfTestWorkers=5
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="mstest">
|
||||
|
||||
By default MSTest will run all classes in parallel, while running tests inside each class sequentially (`ExecutionScope.ClassLevel`). It will create as many processes as there are cores on the host system. You can adjust this behavior by using the following CLI parameter or using a `.runsettings` file, see below.
|
||||
Running tests in parallel at the method level (`ExecutionScope.MethodLevel`) is not supported.
|
||||
|
|
@ -26,7 +64,58 @@ Running tests in parallel at the method level (`ExecutionScope.MethodLevel`) is
|
|||
dotnet test --settings:.runsettings -- MSTest.Parallelize.Workers=4
|
||||
```
|
||||
|
||||
### Customizing [BrowserContext] options
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Customizing [BrowserContext] options
|
||||
|
||||
<Tabs
|
||||
groupId="test-runners"
|
||||
defaultValue="mstest"
|
||||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
||||
To customize context options, you can override the `ContextOptions` method of your test class derived from `Microsoft.Playwright.MSTest.PageTest` or `Microsoft.Playwright.MSTest.ContextTest`. See the following example:
|
||||
|
||||
```csharp
|
||||
using Microsoft.Playwright.NUnit;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
[Parallelizable(ParallelScope.Self)]
|
||||
[TestFixture]
|
||||
public class MyTest : PageTest
|
||||
{
|
||||
[Test]
|
||||
public async Task TestWithCustomContextOptions()
|
||||
{
|
||||
// The following Page (and BrowserContext) instance has the custom colorScheme, viewport and baseURL set:
|
||||
await Page.GotoAsync("/login");
|
||||
}
|
||||
|
||||
public override BrowserNewContextOptions ContextOptions()
|
||||
{
|
||||
return new BrowserNewContextOptions()
|
||||
{
|
||||
ColorScheme = ColorScheme.Light,
|
||||
ViewportSize = new()
|
||||
{
|
||||
Width = 1920,
|
||||
Height = 1080
|
||||
},
|
||||
BaseURL = "https://github.com",
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="mstest">
|
||||
|
||||
To customize context options, you can override the `ContextOptions` method of your test class derived from `Microsoft.Playwright.MSTest.PageTest` or `Microsoft.Playwright.MSTest.ContextTest`. See the following example:
|
||||
|
||||
|
|
@ -65,7 +154,11 @@ public class ExampleTest : PageTest
|
|||
|
||||
```
|
||||
|
||||
### Customizing [Browser]/launch options
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
## Customizing [Browser]/launch options
|
||||
|
||||
[Browser]/launch options can be overridden either using a run settings file or by setting the run settings options directly via the
|
||||
CLI. See the following example:
|
||||
|
|
@ -87,14 +180,55 @@ CLI. See the following example:
|
|||
dotnet test -- Playwright.BrowserName=chromium Playwright.LaunchOptions.Headless=false Playwright.LaunchOptions.Channel=msedge
|
||||
```
|
||||
|
||||
### Using Verbose API Logs
|
||||
## Using Verbose API Logs
|
||||
|
||||
When you have enabled the [verbose API log](./debug.md#verbose-api-logs), via the `DEBUG` environment variable, you will see the messages in the standard error stream. In MSTest, within Visual Studio, that will be the `Tests` pane of the `Output` window. It will also be displayed in the `Test Log` for each test.
|
||||
When you have enabled the [verbose API log](./debug.md#verbose-api-logs), via the `DEBUG` environment variable, you will see the messages in the standard error stream. Within Visual Studio, that will be the `Tests` pane of the `Output` window. It will also be displayed in the `Test Log` for each test.
|
||||
|
||||
### Using the .runsettings file
|
||||
## Using the .runsettings file
|
||||
|
||||
When running tests from Visual Studio, you can take advantage of the `.runsettings` file. The following shows a reference of the supported values.
|
||||
|
||||
<Tabs
|
||||
groupId="test-runners"
|
||||
defaultValue="mstest"
|
||||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
||||
For example, to specify the number of workers you can use `NUnit.NumberOfTestWorkers` or to enable `DEBUG` logs `RunConfiguration.EnvironmentVariables`.
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RunSettings>
|
||||
<!-- NUnit adapter -->
|
||||
<NUnit>
|
||||
<NumberOfTestWorkers>24</NumberOfTestWorkers>
|
||||
</NUnit>
|
||||
<!-- General run configuration -->
|
||||
<RunConfiguration>
|
||||
<EnvironmentVariables>
|
||||
<!-- For debugging selectors, it's recommend to set the following environment variable -->
|
||||
<DEBUG>pw:api</DEBUG>
|
||||
</EnvironmentVariables>
|
||||
</RunConfiguration>
|
||||
<!-- Playwright -->
|
||||
<Playwright>
|
||||
<BrowserName>chromium</BrowserName>
|
||||
<ExpectTimeout>5000</ExpectTimeout>
|
||||
<LaunchOptions>
|
||||
<Headless>false</Headless>
|
||||
<Channel>msedge</Channel>
|
||||
</LaunchOptions>
|
||||
</Playwright>
|
||||
</RunSettings>
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="mstest">
|
||||
|
||||
For example, to specify the number of workers, you can use `MSTest.Parallelize.Workers`. You can also enable `DEBUG` logs using `RunConfiguration.EnvironmentVariables`.
|
||||
|
||||
```xml
|
||||
|
|
@ -125,131 +259,30 @@ For example, to specify the number of workers, you can use `MSTest.Parallelize.W
|
|||
</RunSettings>
|
||||
```
|
||||
|
||||
### Base MSTest classes for Playwright
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Base classes for Playwright
|
||||
|
||||
<Tabs
|
||||
groupId="test-runners"
|
||||
defaultValue="mstest"
|
||||
values={[
|
||||
{label: 'MSTest', value: 'mstest'},
|
||||
{label: 'NUnit', value: 'nunit'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="nunit">
|
||||
|
||||
There are a few base classes available to you in `Microsoft.Playwright.NUnit` namespace:
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="mstest">
|
||||
|
||||
There are a few base classes available to you in `Microsoft.Playwright.MSTest` namespace:
|
||||
|
||||
|Test |Description|
|
||||
|--------------|-----------|
|
||||
|PageTest |Each test gets a fresh copy of a web [Page] created in its own unique [BrowserContext]. Extending this class is the simplest way of writing a fully-functional Playwright test.<br></br><br></br>Note: You can override the `ContextOptions` method in each test file to control context options, the ones typically passed into the [`method: Browser.newContext`] method. That way you can specify all kinds of emulation options for your test file individually.|
|
||||
|ContextTest |Each test will get a fresh copy of a [BrowserContext]. You can create as many pages in this context as you'd like. Using this test is the easiest way to test multi-page scenarios where you need more than one tab.<br></br><br></br>Note: You can override the `ContextOptions` method in each test file to control context options, the ones typically passed into the [`method: Browser.newContext`] method. That way you can specify all kinds of emulation options for your test file individually.|
|
||||
|BrowserTest |Each test will get a browser and can create as many contexts as it likes. Each test is responsible for cleaning up all the contexts it created.|
|
||||
|PlaywrightTest|This gives each test a Playwright object so that the test could start and stop as many browsers as it likes.|
|
||||
|
||||
## NUnit
|
||||
|
||||
Playwright provides base classes to write tests with NUnit via the [`Microsoft.Playwright.NUnit`](https://www.nuget.org/packages/Microsoft.Playwright.NUnit) package.
|
||||
|
||||
Check out the [installation guide](./intro.md) to get started.
|
||||
|
||||
### Running NUnit tests in Parallel
|
||||
|
||||
By default NUnit will run all test files in parallel, while running tests inside each file sequentially (`ParallelScope.Self`). It will create as many processes as there are cores on the host system. You can adjust this behavior using the NUnit.NumberOfTestWorkers parameter.
|
||||
Only `ParallelScope.Self` is supported.
|
||||
|
||||
For CPU-bound tests, we recommend using as many workers as there are cores on your system, divided by 2. For IO-bound tests you can use as many workers as you have cores.
|
||||
|
||||
```bash
|
||||
dotnet test -- NUnit.NumberOfTestWorkers=5
|
||||
```
|
||||
|
||||
### Customizing [BrowserContext] options
|
||||
|
||||
To customize context options, you can override the `ContextOptions` method of your test class derived from `Microsoft.Playwright.MSTest.PageTest` or `Microsoft.Playwright.MSTest.ContextTest`. See the following example:
|
||||
|
||||
```csharp
|
||||
using Microsoft.Playwright.NUnit;
|
||||
|
||||
namespace PlaywrightTests;
|
||||
|
||||
[Parallelizable(ParallelScope.Self)]
|
||||
[TestFixture]
|
||||
public class MyTest : PageTest
|
||||
{
|
||||
[Test]
|
||||
public async Task TestWithCustomContextOptions()
|
||||
{
|
||||
// The following Page (and BrowserContext) instance has the custom colorScheme, viewport and baseURL set:
|
||||
await Page.GotoAsync("/login");
|
||||
}
|
||||
|
||||
public override BrowserNewContextOptions ContextOptions()
|
||||
{
|
||||
return new BrowserNewContextOptions()
|
||||
{
|
||||
ColorScheme = ColorScheme.Light,
|
||||
ViewportSize = new()
|
||||
{
|
||||
Width = 1920,
|
||||
Height = 1080
|
||||
},
|
||||
BaseURL = "https://github.com",
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Customizing [Browser]/launch options
|
||||
|
||||
[Browser]/launch options can be overridden either using a run settings file or by setting the run settings options directly via the
|
||||
CLI. See the following example:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RunSettings>
|
||||
<Playwright>
|
||||
<BrowserName>chromium</BrowserName>
|
||||
<LaunchOptions>
|
||||
<Headless>false</Headless>
|
||||
<Channel>msedge</Channel>
|
||||
</LaunchOptions>
|
||||
</Playwright>
|
||||
</RunSettings>
|
||||
```
|
||||
|
||||
```bash
|
||||
dotnet test -- Playwright.BrowserName=chromium Playwright.LaunchOptions.Headless=false Playwright.LaunchOptions.Channel=msedge
|
||||
```
|
||||
|
||||
### Using Verbose API Logs
|
||||
|
||||
When you have enabled the [verbose API log](./debug.md#verbose-api-logs), via the `DEBUG` environment variable, you will see the messages in the standard error stream. In NUnit, within Visual Studio, that will be the `Tests` pane of the `Output` window. It will also be displayed in the `Test Log` for each test.
|
||||
|
||||
### Using the .runsettings file
|
||||
|
||||
When running tests from Visual Studio, you can take advantage of the `.runsettings` file. The following shows a reference of the supported values.
|
||||
|
||||
For example, to specify the amount of workers you can use `NUnit.NumberOfTestWorkers` or to enable `DEBUG` logs `RunConfiguration.EnvironmentVariables`.
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RunSettings>
|
||||
<!-- NUnit adapter -->
|
||||
<NUnit>
|
||||
<NumberOfTestWorkers>24</NumberOfTestWorkers>
|
||||
</NUnit>
|
||||
<!-- General run configuration -->
|
||||
<RunConfiguration>
|
||||
<EnvironmentVariables>
|
||||
<!-- For debugging selectors, it's recommend to set the following environment variable -->
|
||||
<DEBUG>pw:api</DEBUG>
|
||||
</EnvironmentVariables>
|
||||
</RunConfiguration>
|
||||
<!-- Playwright -->
|
||||
<Playwright>
|
||||
<BrowserName>chromium</BrowserName>
|
||||
<ExpectTimeout>5000</ExpectTimeout>
|
||||
<LaunchOptions>
|
||||
<Headless>false</Headless>
|
||||
<Channel>msedge</Channel>
|
||||
</LaunchOptions>
|
||||
</Playwright>
|
||||
</RunSettings>
|
||||
```
|
||||
|
||||
### Base NUnit classes for Playwright
|
||||
|
||||
There are a few base classes available to you in `Microsoft.Playwright.NUnit` namespace:
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|Test |Description|
|
||||
|--------------|-----------|
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ window.onload = () => {
|
|||
ReactDOM.createRoot(document.querySelector('#root')!).render(<ReportLoader />);
|
||||
};
|
||||
|
||||
const kPlaywrightReportStorageForHMR = 'playwrightReportStorageForHMR';
|
||||
|
||||
class ZipReport implements LoadedReport {
|
||||
private _entries = new Map<string, zip.Entry>();
|
||||
private _json!: HTMLReport;
|
||||
|
|
@ -58,8 +60,20 @@ class ZipReport implements LoadedReport {
|
|||
const zipURI = await new Promise<string>(resolve => {
|
||||
if (window.playwrightReportBase64)
|
||||
return resolve(window.playwrightReportBase64);
|
||||
window.addEventListener('message', event => event.source === window.opener && resolve(event.data), { once: true });
|
||||
window.opener.postMessage('ready', '*');
|
||||
if (window.opener) {
|
||||
window.addEventListener('message', event => {
|
||||
if (event.source === window.opener) {
|
||||
localStorage.setItem(kPlaywrightReportStorageForHMR, event.data);
|
||||
resolve(event.data);
|
||||
}
|
||||
}, { once: true });
|
||||
window.opener.postMessage('ready', '*');
|
||||
} else {
|
||||
const oldReport = localStorage.getItem(kPlaywrightReportStorageForHMR);
|
||||
if (oldReport)
|
||||
return resolve(oldReport);
|
||||
alert('couldnt find report, something with HMR is broken');
|
||||
}
|
||||
});
|
||||
|
||||
const zipReader = new zipjs.ZipReader(new zipjs.Data64URIReader(zipURI), { useWebWorkers: false });
|
||||
|
|
|
|||
|
|
@ -3,21 +3,21 @@
|
|||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1149",
|
||||
"revision": "1150",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "132.0.6834.6"
|
||||
"browserVersion": "132.0.6834.15"
|
||||
},
|
||||
{
|
||||
"name": "chromium-headless-shell",
|
||||
"revision": "1149",
|
||||
"revision": "1150",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "132.0.6834.6"
|
||||
"browserVersion": "132.0.6834.15"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1279",
|
||||
"revision": "1280",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "133.0.6846.0"
|
||||
"browserVersion": "133.0.6850.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
|
|
@ -33,9 +33,11 @@
|
|||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2105",
|
||||
"revision": "2110",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"debian11-x64": "2105",
|
||||
"debian11-arm64": "2105",
|
||||
"mac10.14": "1446",
|
||||
"mac10.15": "1616",
|
||||
"mac11": "1816",
|
||||
|
|
|
|||
|
|
@ -731,6 +731,10 @@ class FrameSession {
|
|||
if (!frame)
|
||||
return; // Subtree may be already gone due to renderer/browser race.
|
||||
this._page._frameManager.removeChildFramesRecursively(frame);
|
||||
for (const [contextId, context] of this._contextIdToContext) {
|
||||
if (context.frame === frame)
|
||||
this._onExecutionContextDestroyed(contextId);
|
||||
}
|
||||
const frameSession = new FrameSession(this._crPage, session, targetId, this);
|
||||
this._crPage._sessions.set(targetId, frameSession);
|
||||
frameSession._initialize(false).catch(e => e);
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Galaxy S5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S8": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 740
|
||||
|
|
@ -143,7 +143,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S8 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 740,
|
||||
"height": 360
|
||||
|
|
@ -154,7 +154,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S9+": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 320,
|
||||
"height": 658
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S9+ landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 658,
|
||||
"height": 320
|
||||
|
|
@ -176,7 +176,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy Tab S4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Safari/537.36",
|
||||
"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",
|
||||
"viewport": {
|
||||
"width": 712,
|
||||
"height": 1138
|
||||
|
|
@ -187,7 +187,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy Tab S4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Safari/537.36",
|
||||
"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",
|
||||
"viewport": {
|
||||
"width": 1138,
|
||||
"height": 712
|
||||
|
|
@ -1098,7 +1098,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"LG Optimus L70": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1109,7 +1109,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"LG Optimus L70 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1120,7 +1120,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 550": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1131,7 +1131,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 550 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1142,7 +1142,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 950": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1153,7 +1153,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 950 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1164,7 +1164,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 10": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 800,
|
||||
"height": 1280
|
||||
|
|
@ -1175,7 +1175,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 10 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 1280,
|
||||
"height": 800
|
||||
|
|
@ -1186,7 +1186,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1197,7 +1197,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1208,7 +1208,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1219,7 +1219,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1230,7 +1230,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5X": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1241,7 +1241,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5X landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1252,7 +1252,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1263,7 +1263,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1274,7 +1274,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6P": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1285,7 +1285,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6P landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1296,7 +1296,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 7": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 600,
|
||||
"height": 960
|
||||
|
|
@ -1307,7 +1307,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 7 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 960,
|
||||
"height": 600
|
||||
|
|
@ -1362,7 +1362,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Pixel 2": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 411,
|
||||
"height": 731
|
||||
|
|
@ -1373,7 +1373,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 731,
|
||||
"height": 411
|
||||
|
|
@ -1384,7 +1384,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 XL": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 411,
|
||||
"height": 823
|
||||
|
|
@ -1395,7 +1395,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 XL landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 823,
|
||||
"height": 411
|
||||
|
|
@ -1406,7 +1406,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 3": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 393,
|
||||
"height": 786
|
||||
|
|
@ -1417,7 +1417,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 3 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 786,
|
||||
"height": 393
|
||||
|
|
@ -1428,7 +1428,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 353,
|
||||
"height": 745
|
||||
|
|
@ -1439,7 +1439,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 745,
|
||||
"height": 353
|
||||
|
|
@ -1450,7 +1450,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4a (5G)": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Mobile Safari/537.36",
|
||||
"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",
|
||||
"screen": {
|
||||
"width": 412,
|
||||
"height": 892
|
||||
|
|
@ -1465,7 +1465,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4a (5G) landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Mobile Safari/537.36",
|
||||
"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",
|
||||
"screen": {
|
||||
"height": 892,
|
||||
"width": 412
|
||||
|
|
@ -1480,7 +1480,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 393,
|
||||
"height": 851
|
||||
|
|
@ -1495,7 +1495,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 851,
|
||||
"height": 393
|
||||
|
|
@ -1510,7 +1510,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 7": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 412,
|
||||
"height": 915
|
||||
|
|
@ -1525,7 +1525,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 7 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 915,
|
||||
"height": 412
|
||||
|
|
@ -1540,7 +1540,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Moto G4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1551,7 +1551,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Moto G4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 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.15 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1562,7 +1562,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Chrome HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Safari/537.36",
|
||||
"screen": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1577,7 +1577,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Edge HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Safari/537.36 Edg/132.0.6834.6",
|
||||
"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",
|
||||
"screen": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1622,7 +1622,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Desktop Chrome": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.15 Safari/537.36",
|
||||
"screen": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
@ -1637,7 +1637,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Edge": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.6 Safari/537.36 Edg/132.0.6834.6",
|
||||
"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",
|
||||
"screen": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
|
|||
|
|
@ -942,7 +942,7 @@ export class Frame extends SdkObject {
|
|||
origin(): string | undefined {
|
||||
if (!this._url.startsWith('http'))
|
||||
return;
|
||||
return network.parsedURL(this._url)?.origin;
|
||||
return network.parseURL(this._url)?.origin;
|
||||
}
|
||||
|
||||
parentFrame(): Frame | null {
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ export class HarTracer {
|
|||
const page = request.frame()?._page;
|
||||
if (this._page && page !== this._page)
|
||||
return;
|
||||
const url = network.parsedURL(request.url());
|
||||
const url = network.parseURL(request.url());
|
||||
if (!url)
|
||||
return;
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import type { CSSComplexSelectorList } from '../../utils/isomorphic/cssParser';
|
|||
import { generateSelector, type GenerateSelectorOptions } from './selectorGenerator';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import { Highlight } from './highlight';
|
||||
import { getChecked, getAriaDisabled, getAriaRole, getElementAccessibleName, getElementAccessibleDescription } from './roleUtils';
|
||||
import { getChecked, getAriaDisabled, getAriaRole, getElementAccessibleName, getElementAccessibleDescription, getReadonly } from './roleUtils';
|
||||
import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils';
|
||||
import { asLocator } from '../../utils/isomorphic/locatorGenerators';
|
||||
import type { Language } from '../../utils/isomorphic/locatorGenerators';
|
||||
|
|
@ -626,9 +626,12 @@ export class InjectedScript {
|
|||
if (state === 'enabled')
|
||||
return !disabled;
|
||||
|
||||
const editable = !(['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName) && element.hasAttribute('readonly'));
|
||||
if (state === 'editable')
|
||||
return !disabled && editable;
|
||||
if (state === 'editable') {
|
||||
const readonly = getReadonly(element);
|
||||
if (readonly === 'error')
|
||||
throw this.createStacklessError('Element is not an <input>, <textarea>, <select> or [contenteditable] and does not have a role allowing [aria-readonly]');
|
||||
return !disabled && !readonly;
|
||||
}
|
||||
|
||||
if (state === 'checked' || state === 'unchecked') {
|
||||
const need = state === 'checked';
|
||||
|
|
|
|||
|
|
@ -860,6 +860,21 @@ export function getChecked(element: Element, allowMixed: boolean): boolean | 'mi
|
|||
return 'error';
|
||||
}
|
||||
|
||||
// https://w3c.github.io/aria/#aria-readonly
|
||||
const kAriaReadonlyRoles = ['checkbox', 'combobox', 'grid', 'gridcell', 'listbox', 'radiogroup', 'slider', 'spinbutton', 'textbox', 'columnheader', 'rowheader', 'searchbox', 'switch', 'treegrid'];
|
||||
export function getReadonly(element: Element): boolean | 'error' {
|
||||
const tagName = elementSafeTagName(element);
|
||||
// https://www.w3.org/TR/wai-aria-1.2/#aria-checked
|
||||
// https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
|
||||
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tagName))
|
||||
return element.hasAttribute('readonly');
|
||||
if (kAriaReadonlyRoles.includes(getAriaRole(element) || ''))
|
||||
return element.getAttribute('aria-readonly') === 'true';
|
||||
if ((element as HTMLElement).isContentEditable)
|
||||
return false;
|
||||
return 'error';
|
||||
}
|
||||
|
||||
export const kAriaPressedRoles = ['button'];
|
||||
export function getAriaPressed(element: Element): boolean | 'mixed' {
|
||||
// https://www.w3.org/TR/wai-aria-1.2/#aria-pressed
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export function rewriteCookies(cookies: channels.SetNetworkCookie[]): channels.S
|
|||
});
|
||||
}
|
||||
|
||||
export function parsedURL(url: string): URL | null {
|
||||
export function parseURL(url: string): URL | null {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -329,7 +329,7 @@ export const deps: any = {
|
|||
'libgstreamer-gl1.0-0',
|
||||
'libgstreamer-plugins-base1.0-0',
|
||||
'libgstreamer1.0-0',
|
||||
'libgtk-3-0',
|
||||
'libgtk-4-1',
|
||||
'libgudev-1.0-0',
|
||||
'libharfbuzz-icu0',
|
||||
'libharfbuzz0b',
|
||||
|
|
@ -400,6 +400,7 @@ export const deps: any = {
|
|||
'libgsttag-1.0.so.0': 'libgstreamer-plugins-base1.0-0',
|
||||
'libgstvideo-1.0.so.0': 'libgstreamer-plugins-base1.0-0',
|
||||
'libgtk-3.so.0': 'libgtk-3-0',
|
||||
'libgtk-4.so.1': 'libgtk-4-1',
|
||||
'libgudev-1.0.so.0': 'libgudev-1.0-0',
|
||||
'libharfbuzz-icu.so.0': 'libharfbuzz-icu0',
|
||||
'libharfbuzz.so.0': 'libharfbuzz0b',
|
||||
|
|
@ -544,7 +545,7 @@ export const deps: any = {
|
|||
'libgstreamer-plugins-bad1.0-0',
|
||||
'libgstreamer-plugins-base1.0-0',
|
||||
'libgstreamer1.0-0',
|
||||
'libgtk-3-0t64',
|
||||
'libgtk-4-1',
|
||||
'libharfbuzz-icu0',
|
||||
'libharfbuzz0b',
|
||||
'libhyphen0',
|
||||
|
|
@ -621,6 +622,7 @@ export const deps: any = {
|
|||
'libgsttag-1.0.so.0': 'libgstreamer-plugins-base1.0-0',
|
||||
'libgstvideo-1.0.so.0': 'libgstreamer-plugins-base1.0-0',
|
||||
'libgtk-3.so.0': 'libgtk-3-0t64',
|
||||
'libgtk-4.so.1': 'libgtk-4-1',
|
||||
'libharfbuzz-icu.so.0': 'libharfbuzz-icu0',
|
||||
'libharfbuzz.so.0': 'libharfbuzz0b',
|
||||
'libhyphen.so.0': 'libhyphen0',
|
||||
|
|
@ -967,7 +969,7 @@ export const deps: any = {
|
|||
'libgstreamer-gl1.0-0',
|
||||
'libgstreamer-plugins-base1.0-0',
|
||||
'libgstreamer1.0-0',
|
||||
'libgtk-3-0',
|
||||
'libgtk-4-1',
|
||||
'libgudev-1.0-0',
|
||||
'libharfbuzz-icu0',
|
||||
'libharfbuzz0b',
|
||||
|
|
@ -1028,6 +1030,7 @@ export const deps: any = {
|
|||
'libXfixes.so.3': 'libxfixes3',
|
||||
'libxkbcommon.so.0': 'libxkbcommon0',
|
||||
'libXrandr.so.2': 'libxrandr2',
|
||||
'libgtk-4.so.1': 'libgtk-4-1',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
|
|||
const kCustomElementsAttribute = '__playwright_custom_elements__';
|
||||
const kCurrentSrcAttribute = '__playwright_current_src__';
|
||||
const kBoundingRectAttribute = '__playwright_bounding_rect__';
|
||||
const kPopoverOpenAttribute = '__playwright_popover_open_';
|
||||
|
||||
// Symbols for our own info on Nodes/StyleSheets.
|
||||
const kSnapshotFrameId = Symbol('__playwright_snapshot_frameid_');
|
||||
|
|
@ -449,6 +450,12 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
|
|||
expectValue(value);
|
||||
attrs[kBoundingRectAttribute] = value;
|
||||
}
|
||||
if ((element as HTMLElement).popover && (element as HTMLElement).matches && (element as HTMLElement).matches(':popover-open')) {
|
||||
const value = 'true';
|
||||
expectValue(kPopoverOpenAttribute);
|
||||
expectValue(value);
|
||||
attrs[kPopoverOpenAttribute] = value;
|
||||
}
|
||||
if (element.scrollTop) {
|
||||
expectValue(kScrollTopAttribute);
|
||||
expectValue(element.scrollTop);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export class WebKit extends BrowserType {
|
|||
override doRewriteStartupLog(error: ProtocolError): ProtocolError {
|
||||
if (!error.logs)
|
||||
return error;
|
||||
if (error.logs.includes('cannot open display'))
|
||||
if (error.logs.includes('Failed to open display') || error.logs.includes('cannot open display'))
|
||||
error.logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
|
||||
return error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export function getComparator(mimeType: string): Comparator {
|
|||
|
||||
const JPEG_JS_MAX_BUFFER_SIZE_IN_MB = 5 * 1024; // ~5 GB
|
||||
|
||||
function compareBuffersOrStrings(actualBuffer: Buffer | string, expectedBuffer: Buffer): ComparatorResult {
|
||||
export function compareBuffersOrStrings(actualBuffer: Buffer | string, expectedBuffer: Buffer): ComparatorResult {
|
||||
if (typeof actualBuffer === 'string')
|
||||
return compareText(actualBuffer, expectedBuffer);
|
||||
if (!actualBuffer || !(actualBuffer instanceof Buffer))
|
||||
|
|
@ -109,26 +109,27 @@ function validateBuffer(buffer: Buffer, mimeType: string): void {
|
|||
function compareText(actual: Buffer | string, expectedBuffer: Buffer): ComparatorResult {
|
||||
if (typeof actual !== 'string')
|
||||
return { errorMessage: 'Actual result should be a string' };
|
||||
const expected = expectedBuffer.toString('utf-8');
|
||||
let expected = expectedBuffer.toString('utf-8');
|
||||
if (expected === actual)
|
||||
return null;
|
||||
const diffs = diff.diffChars(expected, actual);
|
||||
return {
|
||||
errorMessage: diff_prettyTerminal(diffs),
|
||||
};
|
||||
}
|
||||
// Eliminate '\\ No newline at end of file'
|
||||
if (!actual.endsWith('\n'))
|
||||
actual += '\n';
|
||||
if (!expected.endsWith('\n'))
|
||||
expected += '\n';
|
||||
|
||||
function diff_prettyTerminal(diffs: Diff.Change[]): string {
|
||||
const result = diffs.map(part => {
|
||||
const text = part.value;
|
||||
if (part.added)
|
||||
return colors.green(text);
|
||||
else if (part.removed)
|
||||
return colors.reset(colors.strikethrough(colors.red(text)));
|
||||
else
|
||||
return text;
|
||||
const lines = diff.createPatch('file', expected, actual, undefined, undefined, { context: 5 }).split('\n');
|
||||
const coloredLines = lines.slice(4).map(line => {
|
||||
if (line.startsWith('-'))
|
||||
return colors.red(line);
|
||||
if (line.startsWith('+'))
|
||||
return colors.green(line);
|
||||
if (line.startsWith('@@'))
|
||||
return colors.dim(line);
|
||||
return line;
|
||||
});
|
||||
return result.join('');
|
||||
const errorMessage = coloredLines.join('\n');
|
||||
return { errorMessage };
|
||||
}
|
||||
|
||||
function resizeImage(image: ImageData, size: { width: number, height: number }): ImageData {
|
||||
|
|
|
|||
|
|
@ -107,19 +107,15 @@ export function urlMatches(baseURL: string | undefined, urlString: string, match
|
|||
match = globToRegex(match);
|
||||
if (isRegExp(match))
|
||||
return match.test(urlString);
|
||||
if (typeof match === 'string' && match === urlString)
|
||||
return true;
|
||||
const url = parsedURL(urlString);
|
||||
const url = parseURL(urlString);
|
||||
if (!url)
|
||||
return false;
|
||||
if (typeof match === 'string')
|
||||
return url.pathname === match;
|
||||
if (typeof match !== 'function')
|
||||
throw new Error('url parameter should be string, RegExp or function');
|
||||
return match(url);
|
||||
}
|
||||
|
||||
function parsedURL(url: string): URL | null {
|
||||
function parseURL(url: string): URL | null {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (e) {
|
||||
|
|
|
|||
4
packages/playwright-core/types/types.d.ts
vendored
4
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -13680,7 +13680,9 @@ export interface Locator {
|
|||
}): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Returns whether the element is [editable](https://playwright.dev/docs/actionability#editable).
|
||||
* Returns whether the element is [editable](https://playwright.dev/docs/actionability#editable). If the target element is not an `<input>`,
|
||||
* `<textarea>`, `<select>`, `[contenteditable]` and does not have a role allowing `[aria-readonly]`, this method
|
||||
* throws an error.
|
||||
*
|
||||
* **NOTE** If you need to assert that an element is editable, prefer
|
||||
* [expect(locator).toBeEditable([options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-be-editable)
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ export class FullConfigInternal {
|
|||
projects: [],
|
||||
shard: takeFirst(configCLIOverrides.shard, userConfig.shard, null),
|
||||
updateSnapshots: takeFirst(configCLIOverrides.updateSnapshots, userConfig.updateSnapshots, 'missing'),
|
||||
updateSourceMethod: takeFirst(configCLIOverrides.updateSourceMethod, userConfig.updateSourceMethod, 'patch'),
|
||||
version: require('../../package.json').version,
|
||||
workers: 0,
|
||||
webServer: null,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ export type ConfigCLIOverrides = {
|
|||
timeout?: number;
|
||||
tsconfig?: string;
|
||||
ignoreSnapshots?: boolean;
|
||||
updateSnapshots?: 'all'|'none'|'missing';
|
||||
updateSnapshots?: 'all'|'changed'|'missing'|'none';
|
||||
updateSourceMethod?: 'overwrite'|'patch'|'3way';
|
||||
workers?: number | string;
|
||||
projects?: { name: string, use?: any }[],
|
||||
use?: any;
|
||||
|
|
|
|||
|
|
@ -594,6 +594,7 @@ export const baseFullConfig: reporterTypes.FullConfig = {
|
|||
quiet: false,
|
||||
shard: null,
|
||||
updateSnapshots: 'missing',
|
||||
updateSourceMethod: 'patch',
|
||||
version: '',
|
||||
workers: 0,
|
||||
webServer: null,
|
||||
|
|
|
|||
|
|
@ -57,8 +57,6 @@ export async function toMatchAriaSnapshot(
|
|||
}
|
||||
|
||||
const generateMissingBaseline = updateSnapshots === 'missing' && !expected;
|
||||
const generateNewBaseline = updateSnapshots === 'all' || generateMissingBaseline;
|
||||
|
||||
if (generateMissingBaseline) {
|
||||
if (this.isNot) {
|
||||
const message = `Matchers using ".not" can't generate new baselines`;
|
||||
|
|
@ -100,10 +98,13 @@ export async function toMatchAriaSnapshot(
|
|||
}
|
||||
};
|
||||
|
||||
if (!this.isNot && pass === this.isNot && generateNewBaseline) {
|
||||
// Only rebaseline failed snapshots.
|
||||
const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\`)`;
|
||||
return { pass: this.isNot, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
|
||||
if (!this.isNot) {
|
||||
if ((updateSnapshots === 'all') ||
|
||||
(updateSnapshots === 'changed' && pass === this.isNot) ||
|
||||
generateMissingBaseline) {
|
||||
const suggestedRebaseline = `toMatchAriaSnapshot(\`\n${escapeTemplateString(indent(typedReceived.regex, '{indent} '))}\n{indent}\`)`;
|
||||
return { pass: this.isNot, message: () => '', name: 'toMatchAriaSnapshot', suggestedRebaseline };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import type { Locator, Page } from 'playwright-core';
|
|||
import type { ExpectScreenshotOptions, Page as PageEx } from 'playwright-core/lib/client/page';
|
||||
import { currentTestInfo } from '../common/globals';
|
||||
import type { ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils';
|
||||
import { getComparator, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
|
||||
import { compareBuffersOrStrings, getComparator, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
|
||||
import {
|
||||
addSuffixToFilePath,
|
||||
trimLongString, callLogText,
|
||||
|
|
@ -83,7 +83,7 @@ class SnapshotHelper {
|
|||
readonly diffPath: string;
|
||||
readonly mimeType: string;
|
||||
readonly kind: 'Screenshot'|'Snapshot';
|
||||
readonly updateSnapshots: 'all' | 'none' | 'missing';
|
||||
readonly updateSnapshots: 'all' | 'changed' | 'missing' | 'none';
|
||||
readonly comparator: Comparator;
|
||||
readonly options: Omit<ToHaveScreenshotOptions, '_comparator'> & { comparator?: string };
|
||||
readonly matcherName: string;
|
||||
|
|
@ -199,7 +199,7 @@ class SnapshotHelper {
|
|||
}
|
||||
|
||||
handleMissingNegated(): ImageMatcherResult {
|
||||
const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing';
|
||||
const isWriteMissingMode = this.updateSnapshots !== 'none';
|
||||
const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', matchers using ".not" won\'t write them automatically.' : '.'}`;
|
||||
// NOTE: 'isNot' matcher implies inversed value.
|
||||
return this.createMatcherResult(message, true);
|
||||
|
|
@ -221,14 +221,14 @@ class SnapshotHelper {
|
|||
}
|
||||
|
||||
handleMissing(actual: Buffer | string): ImageMatcherResult {
|
||||
const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing';
|
||||
const isWriteMissingMode = this.updateSnapshots !== 'none';
|
||||
if (isWriteMissingMode)
|
||||
writeFileSync(this.expectedPath, actual);
|
||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
||||
writeFileSync(this.actualPath, actual);
|
||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
||||
const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', writing actual.' : '.'}`;
|
||||
if (this.updateSnapshots === 'all') {
|
||||
if (this.updateSnapshots === 'all' || this.updateSnapshots === 'changed') {
|
||||
/* eslint-disable no-console */
|
||||
console.log(message);
|
||||
return this.createMatcherResult(message, true);
|
||||
|
|
@ -317,17 +317,30 @@ export function toMatchSnapshot(
|
|||
return helper.handleMissing(received);
|
||||
|
||||
const expected = fs.readFileSync(helper.expectedPath);
|
||||
const result = helper.comparator(received, expected, helper.options);
|
||||
if (!result)
|
||||
return helper.handleMatching();
|
||||
|
||||
if (helper.updateSnapshots === 'all') {
|
||||
if (!compareBuffersOrStrings(received, expected))
|
||||
return helper.handleMatching();
|
||||
writeFileSync(helper.expectedPath, received);
|
||||
/* eslint-disable no-console */
|
||||
console.log(helper.expectedPath + ' is not the same, writing actual.');
|
||||
return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true);
|
||||
}
|
||||
|
||||
if (helper.updateSnapshots === 'changed') {
|
||||
const result = helper.comparator(received, expected, helper.options);
|
||||
if (!result)
|
||||
return helper.handleMatching();
|
||||
writeFileSync(helper.expectedPath, received);
|
||||
/* eslint-disable no-console */
|
||||
console.log(helper.expectedPath + ' does not match, writing actual.');
|
||||
return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true);
|
||||
}
|
||||
|
||||
const result = helper.comparator(received, expected, helper.options);
|
||||
if (!result)
|
||||
return helper.handleMatching();
|
||||
|
||||
const receiver = isString(received) ? 'string' : 'Buffer';
|
||||
const header = matcherHint(this, undefined, 'toMatchSnapshot', receiver, undefined, undefined);
|
||||
return helper.handleDifferent(received, expected, undefined, result.diff, header, result.errorMessage, undefined);
|
||||
|
|
@ -421,21 +434,30 @@ export async function toHaveScreenshot(
|
|||
// General case:
|
||||
// - snapshot exists
|
||||
// - regular matcher (i.e. not a `.not`)
|
||||
// - perhaps an 'all' flag to update non-matching screenshots
|
||||
expectScreenshotOptions.expected = await fs.promises.readFile(helper.expectedPath);
|
||||
const expected = await fs.promises.readFile(helper.expectedPath);
|
||||
expectScreenshotOptions.expected = helper.updateSnapshots === 'all' ? undefined : expected;
|
||||
|
||||
const { actual, previous, diff, errorMessage, log, timedOut } = await page._expectScreenshot(expectScreenshotOptions);
|
||||
|
||||
if (!errorMessage)
|
||||
return helper.handleMatching();
|
||||
|
||||
if (helper.updateSnapshots === 'all') {
|
||||
const writeFiles = () => {
|
||||
writeFileSync(helper.expectedPath, actual!);
|
||||
writeFileSync(helper.actualPath, actual!);
|
||||
/* eslint-disable no-console */
|
||||
console.log(helper.expectedPath + ' is re-generated, writing actual.');
|
||||
return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true);
|
||||
};
|
||||
|
||||
if (!errorMessage) {
|
||||
// Screenshot is matching, but is not necessarily the same as the expected.
|
||||
if (helper.updateSnapshots === 'all' && actual && compareBuffersOrStrings(actual, expected)) {
|
||||
console.log(helper.expectedPath + ' is re-generated, writing actual.');
|
||||
return writeFiles();
|
||||
}
|
||||
return helper.handleMatching();
|
||||
}
|
||||
|
||||
if (helper.updateSnapshots === 'changed' || helper.updateSnapshots === 'all')
|
||||
return writeFiles();
|
||||
|
||||
const header = matcherHint(this, undefined, 'toHaveScreenshot', receiver, undefined, undefined, timedOut ? timeout : undefined);
|
||||
return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -281,6 +281,13 @@ async function mergeReports(reportDir: string | undefined, opts: { [key: string]
|
|||
|
||||
function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrides {
|
||||
const shardPair = options.shard ? options.shard.split('/').map((t: string) => parseInt(t, 10)) : undefined;
|
||||
|
||||
let updateSnapshots: 'all' | 'changed' | 'missing' | 'none';
|
||||
if (['all', 'changed', 'missing', 'none'].includes(options.updateSnapshots))
|
||||
updateSnapshots = options.updateSnapshots;
|
||||
else
|
||||
updateSnapshots = 'updateSnapshots' in options ? 'changed' : 'missing';
|
||||
|
||||
const overrides: ConfigCLIOverrides = {
|
||||
forbidOnly: options.forbidOnly ? true : undefined,
|
||||
fullyParallel: options.fullyParallel ? true : undefined,
|
||||
|
|
@ -295,7 +302,8 @@ function overridesFromOptions(options: { [key: string]: any }): ConfigCLIOverrid
|
|||
timeout: options.timeout ? parseInt(options.timeout, 10) : undefined,
|
||||
tsconfig: options.tsconfig ? path.resolve(process.cwd(), options.tsconfig) : undefined,
|
||||
ignoreSnapshots: options.ignoreSnapshots ? !!options.ignoreSnapshots : undefined,
|
||||
updateSnapshots: options.updateSnapshots ? 'all' as const : undefined,
|
||||
updateSnapshots,
|
||||
updateSourceMethod: options.updateSourceMethod || 'patch',
|
||||
workers: options.workers,
|
||||
};
|
||||
|
||||
|
|
@ -344,8 +352,10 @@ function resolveReporter(id: string) {
|
|||
|
||||
const kTraceModes: TraceMode[] = ['on', 'off', 'on-first-retry', 'on-all-retries', 'retain-on-failure', 'retain-on-first-failure'];
|
||||
|
||||
// Note: update docs/src/test-cli-js.md when you update this, program is the source of truth.
|
||||
|
||||
const testOptions: [string, string][] = [
|
||||
['--browser <browser>', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`],
|
||||
/* deprecated */ ['--browser <browser>', `Browser to use for tests, one of "all", "chromium", "firefox" or "webkit" (default: "chromium")`],
|
||||
['-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`],
|
||||
['--debug', `Run tests with Playwright Inspector. Shortcut for "PWDEBUG=1" environment variable and "--timeout=0 --max-failures=1 --headed --workers=1" options`],
|
||||
['--fail-on-flaky-tests', `Fail if any test is flagged as flaky (default: false)`],
|
||||
|
|
@ -375,7 +385,8 @@ const testOptions: [string, string][] = [
|
|||
['--ui', `Run tests in interactive UI mode`],
|
||||
['--ui-host <host>', 'Host to serve UI on; specifying this option opens UI in a browser tab'],
|
||||
['--ui-port <port>', 'Port to serve UI on, 0 for any free port; specifying this option opens UI in a browser tab'],
|
||||
['-u, --update-snapshots', `Update snapshots with actual results (default: only create missing snapshots)`],
|
||||
['-u, --update-snapshots [mode]', `Update snapshots with actual results. Possible values are 'all', 'changed', 'missing' and 'none'. Not passing defaults to 'missing', passing without value defaults to 'changed'`],
|
||||
['--update-source-method <method>', `Chooses the way source is updated. Possible values are 'overwrite', '3way' and 'patch'. Defaults to 'patch'`],
|
||||
['-j, --workers <workers>', `Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker (default: 50%)`],
|
||||
['-x', `Stop after the first failure`],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export function addSuggestedRebaseline(location: Location, suggestedRebaseline:
|
|||
}
|
||||
|
||||
export async function applySuggestedRebaselines(config: FullConfigInternal, reporter: InternalReporter) {
|
||||
if (config.config.updateSnapshots !== 'all' && config.config.updateSnapshots !== 'missing')
|
||||
if (config.config.updateSnapshots === 'none')
|
||||
return;
|
||||
if (!suggestedRebaselines.size)
|
||||
return;
|
||||
|
|
@ -54,6 +54,9 @@ export async function applySuggestedRebaselines(config: FullConfigInternal, repo
|
|||
|
||||
const patches: string[] = [];
|
||||
const files: string[] = [];
|
||||
const gitCache = new Map<string, string | null>();
|
||||
|
||||
const patchFile = path.join(project.project.outputDir, 'rebaselines.patch');
|
||||
|
||||
for (const fileName of [...suggestedRebaselines.keys()].sort()) {
|
||||
const source = await fs.promises.readFile(fileName, 'utf8');
|
||||
|
|
@ -98,15 +101,25 @@ export async function applySuggestedRebaselines(config: FullConfigInternal, repo
|
|||
|
||||
const relativeName = path.relative(process.cwd(), fileName);
|
||||
files.push(relativeName);
|
||||
patches.push(createPatch(relativeName, source, result));
|
||||
|
||||
if (config.config.updateSourceMethod === 'overwrite') {
|
||||
await fs.promises.writeFile(fileName, result);
|
||||
} else if (config.config.updateSourceMethod === '3way') {
|
||||
await fs.promises.writeFile(fileName, applyPatchWithConflictMarkers(source, result));
|
||||
} else {
|
||||
const gitFolder = findGitRoot(path.dirname(fileName), gitCache);
|
||||
const relativeToGit = path.relative(gitFolder || process.cwd(), fileName);
|
||||
patches.push(createPatch(relativeToGit, source, result));
|
||||
}
|
||||
}
|
||||
|
||||
const patchFile = path.join(project.project.outputDir, 'rebaselines.patch');
|
||||
await fs.promises.mkdir(path.dirname(patchFile), { recursive: true });
|
||||
await fs.promises.writeFile(patchFile, patches.join('\n'));
|
||||
|
||||
const fileList = files.map(file => ' ' + colors.dim(file)).join('\n');
|
||||
reporter.onStdErr(`\nNew baselines created for:\n\n${fileList}\n\n ` + colors.cyan('git apply ' + path.relative(process.cwd(), patchFile)) + '\n');
|
||||
reporter.onStdErr(`\nNew baselines created for:\n\n${fileList}\n`);
|
||||
if (config.config.updateSourceMethod === 'patch') {
|
||||
await fs.promises.mkdir(path.dirname(patchFile), { recursive: true });
|
||||
await fs.promises.writeFile(patchFile, patches.join('\n'));
|
||||
reporter.onStdErr(`\n ` + colors.cyan('git apply ' + path.relative(process.cwd(), patchFile)) + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
function createPatch(fileName: string, before: string, after: string) {
|
||||
|
|
@ -119,3 +132,62 @@ function createPatch(fileName: string, before: string, after: string) {
|
|||
...text.split('\n').slice(4)
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function findGitRoot(dir: string, cache: Map<string, string | null>): string | null {
|
||||
const result = cache.get(dir);
|
||||
if (result !== undefined)
|
||||
return result;
|
||||
|
||||
const gitPath = path.join(dir, '.git');
|
||||
if (fs.existsSync(gitPath) && fs.lstatSync(gitPath).isDirectory()) {
|
||||
cache.set(dir, dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
const parentDir = path.dirname(dir);
|
||||
if (dir === parentDir) {
|
||||
cache.set(dir, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const parentResult = findGitRoot(parentDir, cache);
|
||||
cache.set(dir, parentResult);
|
||||
return parentResult;
|
||||
}
|
||||
|
||||
function applyPatchWithConflictMarkers(oldText: string, newText: string) {
|
||||
const diffResult = diff.diffLines(oldText, newText);
|
||||
|
||||
let result = '';
|
||||
let conflict = false;
|
||||
|
||||
diffResult.forEach(part => {
|
||||
if (part.added) {
|
||||
if (conflict) {
|
||||
result += part.value;
|
||||
result += '>>>>>>> SNAPSHOT\n';
|
||||
conflict = false;
|
||||
} else {
|
||||
result += '<<<<<<< HEAD\n';
|
||||
result += part.value;
|
||||
result += '=======\n';
|
||||
conflict = true;
|
||||
}
|
||||
} else if (part.removed) {
|
||||
result += '<<<<<<< HEAD\n';
|
||||
result += part.value;
|
||||
result += '=======\n';
|
||||
conflict = true;
|
||||
} else {
|
||||
if (conflict) {
|
||||
result += '>>>>>>> SNAPSHOT\n';
|
||||
conflict = false;
|
||||
}
|
||||
result += part.value;
|
||||
}
|
||||
});
|
||||
|
||||
if (conflict)
|
||||
result += '>>>>>>> SNAPSHOT\n';
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
27
packages/playwright/types/test.d.ts
vendored
27
packages/playwright/types/test.d.ts
vendored
|
|
@ -1665,11 +1665,12 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
|
||||
/**
|
||||
* Whether to update expected snapshots with the actual results produced by the test run. Defaults to `'missing'`.
|
||||
* - `'all'` - All tests that are executed will update snapshots that did not match. Matching snapshots will not be
|
||||
* updated.
|
||||
* - `'none'` - No snapshots are updated.
|
||||
* - `'all'` - All tests that are executed will update snapshots.
|
||||
* - `'changed'` - All tests that are executed will update snapshots that did not match. Matching snapshots will not
|
||||
* be updated.
|
||||
* - `'missing'` - Missing snapshots are created, for example when authoring a new test and running it for the first
|
||||
* time. This is the default.
|
||||
* - `'none'` - No snapshots are updated.
|
||||
*
|
||||
* Learn more about [snapshots](https://playwright.dev/docs/test-snapshots).
|
||||
*
|
||||
|
|
@ -1685,7 +1686,15 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
* ```
|
||||
*
|
||||
*/
|
||||
updateSnapshots?: "all"|"none"|"missing";
|
||||
updateSnapshots?: "all"|"changed"|"missing"|"none";
|
||||
|
||||
/**
|
||||
* Defines how to update the source code snapshots.
|
||||
* - `'overwrite'` - Overwrite the source code snapshot with the actual result.
|
||||
* - `'3way'` - Use a three-way merge to update the source code snapshot.
|
||||
* - `'patch'` - Use a patch to update the source code snapshot. This is the default.
|
||||
*/
|
||||
updateSourceMethod?: "overwrite"|"3way"|"patch";
|
||||
|
||||
/**
|
||||
* The maximum number of concurrent worker processes to use for parallelizing tests. Can also be set as percentage of
|
||||
|
|
@ -1834,7 +1843,13 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
/**
|
||||
* See [testConfig.updateSnapshots](https://playwright.dev/docs/api/class-testconfig#test-config-update-snapshots).
|
||||
*/
|
||||
updateSnapshots: "all"|"none"|"missing";
|
||||
updateSnapshots: "all"|"changed"|"missing"|"none";
|
||||
|
||||
/**
|
||||
* See
|
||||
* [testConfig.updateSourceMethod](https://playwright.dev/docs/api/class-testconfig#test-config-update-source-method).
|
||||
*/
|
||||
updateSourceMethod: "overwrite"|"3way"|"patch";
|
||||
|
||||
/**
|
||||
* Playwright version.
|
||||
|
|
@ -1849,7 +1864,7 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
|
||||
export type TestStatus = 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted';
|
||||
|
||||
type TestDetailsAnnotation = {
|
||||
export type TestDetailsAnnotation = {
|
||||
type: string;
|
||||
description?: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -270,6 +270,13 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
|
|||
(element as HTMLOptionElement).selected = element.getAttribute('__playwright_selected_') === 'true';
|
||||
element.removeAttribute('__playwright_selected_');
|
||||
}
|
||||
for (const element of root.querySelectorAll(`[__playwright_popover_open_]`)) {
|
||||
try {
|
||||
(element as HTMLElement).showPopover();
|
||||
} catch {
|
||||
}
|
||||
element.removeAttribute('__playwright_popover_open_');
|
||||
}
|
||||
|
||||
for (const targetId of targetIds) {
|
||||
for (const target of root.querySelectorAll(`[__playwright_target__="${targetId}"]`)) {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export const androidTest = baseTest.extend<PageTestFixtures & AndroidTestFixture
|
|||
isElectron: [false, { scope: 'worker' }],
|
||||
electronMajorVersion: [0, { scope: 'worker' }],
|
||||
isWebView2: [false, { scope: 'worker' }],
|
||||
isHeadlessShell: [false, { scope: 'worker' }],
|
||||
|
||||
androidDevice: async ({ androidDeviceWorker }, use) => {
|
||||
await closeAllActivities(androidDeviceWorker);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"typescript": "5.6.2",
|
||||
"vite": "^5.2.8",
|
||||
"vue-tsc": "^2.0.21"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export const electronTest = baseTest.extend<TraceViewerFixtures>(traceViewerFixt
|
|||
isAndroid: [false, { scope: 'worker' }],
|
||||
isElectron: [true, { scope: 'worker' }],
|
||||
isWebView2: [false, { scope: 'worker' }],
|
||||
isHeadlessShell: [false, { scope: 'worker' }],
|
||||
|
||||
launchElectronApp: async ({ playwright }, use) => {
|
||||
// This env prevents 'Electron Security Policy' console message.
|
||||
|
|
|
|||
|
|
@ -428,3 +428,38 @@ it('should not crash when clicking a label with a <input type="file"/>', {
|
|||
const fileChooser = await fileChooserPromise;
|
||||
expect(fileChooser.page()).toBe(page);
|
||||
});
|
||||
|
||||
it('should not auto play audio', {
|
||||
annotation: {
|
||||
type: 'issue',
|
||||
description: 'https://github.com/microsoft/playwright/issues/33590'
|
||||
}
|
||||
}, async ({ page, browserName, isWindows }) => {
|
||||
it.fixme(browserName === 'webkit' && isWindows);
|
||||
await page.route('**/*', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/html',
|
||||
body: `
|
||||
<script>
|
||||
async function onLoad() {
|
||||
const log = document.getElementById('log');
|
||||
const audioContext = new AudioContext();
|
||||
const gainNode = new GainNode(audioContext);
|
||||
gainNode.connect(audioContext.destination);
|
||||
gainNode.gain.value = 0.025;
|
||||
const sineNode = new OscillatorNode(audioContext);
|
||||
sineNode.connect(gainNode);
|
||||
sineNode.start();
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
log.innerHTML = 'State: ' + audioContext.state;
|
||||
}
|
||||
</script>
|
||||
<body onload="onLoad()">
|
||||
<div id="log"></div>
|
||||
</body>`,
|
||||
});
|
||||
});
|
||||
await page.goto('http://127.0.0.1/audio.html');
|
||||
await expect(page.locator('#log')).toHaveText('State: suspended');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -101,8 +101,9 @@ it('should change document.activeElement', async ({ page, server }) => {
|
|||
expect(active).toEqual(['INPUT', 'TEXTAREA']);
|
||||
});
|
||||
|
||||
it('should not affect screenshots', async ({ page, server, browserName, headless, isWindows, isHeadlessShell }) => {
|
||||
it('should not affect screenshots', async ({ page, server, browserName, headless, isWindows, isLinux, isHeadlessShell }) => {
|
||||
it.skip(browserName === 'webkit' && isWindows && !headless, 'WebKit/Windows/headed has a larger minimal viewport. See https://github.com/microsoft/playwright/issues/22616');
|
||||
it.skip(browserName === 'webkit' && isLinux && !headless, 'WebKit headed has a larger minimal viewport on gtk4.');
|
||||
it.skip(browserName === 'firefox' && !headless, 'Firefox headed produces a different image');
|
||||
it.fixme(browserName === 'chromium' && !isHeadlessShell, 'https://github.com/microsoft/playwright/issues/33330');
|
||||
|
||||
|
|
|
|||
|
|
@ -1572,3 +1572,20 @@ test('should show only one pointer with multilevel iframes', async ({ page, runA
|
|||
await expect.soft(snapshotFrame.frameLocator('iframe').locator('x-pw-pointer')).not.toBeAttached();
|
||||
await expect.soft(snapshotFrame.frameLocator('iframe').frameLocator('iframe').locator('x-pw-pointer')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show a popover', async ({ runAndTrace, page, server }) => {
|
||||
const traceViewer = await runAndTrace(async () => {
|
||||
await page.setContent(`
|
||||
<button popovertarget="pop">Click me</button>
|
||||
<article id="pop" popover="auto">
|
||||
<div>I'm a popover</div>
|
||||
</article>
|
||||
`);
|
||||
await page.getByRole('button').click();
|
||||
await expect(page.locator('div')).toBeVisible();
|
||||
});
|
||||
|
||||
const snapshot = await traceViewer.snapshotFrame('expect.toBeVisible');
|
||||
const popover = snapshot.locator('#pop');
|
||||
await expect.poll(() => popover.evaluate(e => e.matches(':popover-open'))).toBe(true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ it.describe('element screenshot', () => {
|
|||
expect(screenshot).toMatchSnapshot('screenshot-element-bounding-box.png');
|
||||
});
|
||||
|
||||
it('should take into account padding and border', async ({ page }) => {
|
||||
it('should take into account padding and border', async ({ page, isLinux, headless, browserName }) => {
|
||||
await page.setViewportSize({ width: 500, height: 500 });
|
||||
await page.setContent(`
|
||||
<div style="height: 14px">oooo</div>
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 179 B After Width: | Height: | Size: 332 B |
|
|
@ -138,6 +138,13 @@ test.describe('toBeEditable', () => {
|
|||
const locator = page.locator('input');
|
||||
await expect(locator).not.toBeEditable({ editable: false });
|
||||
});
|
||||
|
||||
test('throws', async ({ page }) => {
|
||||
await page.setContent('<button>');
|
||||
const locator = page.locator('button');
|
||||
const error = await expect(locator).toBeEditable().catch(e => e);
|
||||
expect(error.message).toContain('Element is not an <input>, <textarea>, <select> or [contenteditable] and does not have a role allowing [aria-readonly]');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('toBeEnabled', () => {
|
||||
|
|
|
|||
|
|
@ -119,7 +119,15 @@ it('isEnabled and isDisabled should work', async ({ page }) => {
|
|||
});
|
||||
|
||||
it('isEditable should work', async ({ page }) => {
|
||||
await page.setContent(`<input id=input1 disabled><textarea></textarea><input id=input2>`);
|
||||
await page.setContent(`
|
||||
<input id=input1 disabled>
|
||||
<textarea></textarea>
|
||||
<input id=input2>
|
||||
<div contenteditable="true"></div>
|
||||
<span id=span1 role=textbox aria-readonly=true></span>
|
||||
<span id=span2 role=textbox></span>
|
||||
<button>button</button>
|
||||
`);
|
||||
await page.$eval('textarea', t => t.readOnly = true);
|
||||
const input1 = page.locator('#input1');
|
||||
expect(await input1.isEditable()).toBe(false);
|
||||
|
|
@ -130,6 +138,11 @@ it('isEditable should work', async ({ page }) => {
|
|||
const textarea = page.locator('textarea');
|
||||
expect(await textarea.isEditable()).toBe(false);
|
||||
expect(await page.isEditable('textarea')).toBe(false);
|
||||
expect(await page.locator('div').isEditable()).toBe(true);
|
||||
expect(await page.locator('#span1').isEditable()).toBe(false);
|
||||
expect(await page.locator('#span2').isEditable()).toBe(true);
|
||||
const error = await page.locator('button').isEditable().catch(e => e);
|
||||
expect(error.message).toContain('Element is not an <input>, <textarea>, <select> or [contenteditable] and does not have a role allowing [aria-readonly]');
|
||||
});
|
||||
|
||||
it('isChecked should work', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -238,8 +238,8 @@ it('should fill elements with existing value and selection', async ({ page, serv
|
|||
|
||||
it('should throw nice error without injected script stack when element is not an <input>', async ({ page, server }) => {
|
||||
let error = null;
|
||||
await page.goto(server.PREFIX + '/input/textarea.html');
|
||||
await page.fill('body', '').catch(e => error = e);
|
||||
await page.setContent(`<select><option>value1</option></select>`);
|
||||
await page.fill('select', '').catch(e => error = e);
|
||||
expect(error.message).toContain('page.fill: Error: Element is not an <input>, <textarea> or [contenteditable] element\nCall log:');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -280,12 +280,13 @@ it.describe('page screenshot', () => {
|
|||
expect(screenshot).toMatchSnapshot('screenshot-clip-odd-size.png');
|
||||
});
|
||||
|
||||
it('should work for canvas', async ({ page, server, isElectron, isMac, isLinux, macVersion, browserName, headless }) => {
|
||||
it('should work for canvas', async ({ page, server, isElectron, isMac, isLinux, macVersion, browserName, isHeadlessShell, headless }) => {
|
||||
it.fixme(isElectron && isMac, 'Fails on the bots');
|
||||
it.fixme(browserName === 'webkit' && isLinux && !headless, 'WebKit has slightly different corners on gtk4.');
|
||||
await page.setViewportSize({ width: 500, height: 500 });
|
||||
await page.goto(server.PREFIX + '/screenshots/canvas.html');
|
||||
const screenshot = await page.screenshot();
|
||||
if ((!headless && browserName === 'chromium' && isMac && os.arch() === 'arm64' && macVersion >= 14) ||
|
||||
if ((!isHeadlessShell && browserName === 'chromium' && isMac && os.arch() === 'arm64' && macVersion >= 14) ||
|
||||
(browserName === 'webkit' && isLinux && os.arch() === 'x64'))
|
||||
expect(screenshot).toMatchSnapshot('screenshot-canvas-with-accurate-corners.png');
|
||||
else
|
||||
|
|
|
|||
|
|
@ -36,4 +36,5 @@ export type PageWorkerFixtures = {
|
|||
isAndroid: boolean;
|
||||
isElectron: boolean;
|
||||
isWebView2: boolean;
|
||||
isHeadlessShell: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ test('should work with non-txt extensions', async ({ runInlineTest }) => {
|
|||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output).toContain(`1,2,34`);
|
||||
expect(result.rawOutput).toContain(colors.red('-1,2,3'));
|
||||
expect(result.rawOutput).toContain(colors.green('+1,2,4'));
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -202,8 +203,8 @@ Line7`,
|
|||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output).toContain('Line1');
|
||||
expect(result.rawOutput).toContain('Line2' + colors.green('2'));
|
||||
expect(result.rawOutput).toContain('line' + colors.reset(colors.strikethrough(colors.red('1'))) + colors.green('2'));
|
||||
expect(result.rawOutput).toContain(colors.red('-Line2'));
|
||||
expect(result.rawOutput).toContain(colors.green('+Line22'));
|
||||
expect(result.output).toContain('Line3');
|
||||
expect(result.output).toContain('Line5');
|
||||
expect(result.output).toContain('Line7');
|
||||
|
|
|
|||
|
|
@ -916,7 +916,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
]));
|
||||
});
|
||||
|
||||
test('should strikethrough textual diff', async ({ runInlineTest, showReport, page }) => {
|
||||
test('should highlight textual diff', async ({ runInlineTest, showReport, page }) => {
|
||||
const result = await runInlineTest({
|
||||
'helper.ts': `
|
||||
import { test as base } from '@playwright/test';
|
||||
|
|
@ -940,36 +940,8 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
|||
await showReport();
|
||||
await page.click('text="is a test"');
|
||||
|
||||
await expect(page.locator('.test-error-view').getByText('old')).toHaveCSS('text-decoration', 'line-through solid rgb(205, 49, 49)');
|
||||
await expect(page.locator('.test-error-view').getByText('new', { exact: true })).toHaveCSS('text-decoration', 'none solid rgb(0, 188, 0)');
|
||||
});
|
||||
|
||||
test('should strikethrough textual diff with commonalities', async ({ runInlineTest, showReport, page }) => {
|
||||
const result = await runInlineTest({
|
||||
'helper.ts': `
|
||||
import { test as base } from '@playwright/test';
|
||||
export * from '@playwright/test';
|
||||
export const test = base.extend({
|
||||
auto: [ async ({}, run, testInfo) => {
|
||||
testInfo.snapshotSuffix = '';
|
||||
await run();
|
||||
}, { auto: true } ]
|
||||
});
|
||||
`,
|
||||
'a.spec.js-snapshots/snapshot.txt': `oldcommon`,
|
||||
'a.spec.js': `
|
||||
const { test, expect } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('newcommon').toMatchSnapshot('snapshot.txt');
|
||||
});
|
||||
`
|
||||
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
|
||||
expect(result.exitCode).toBe(1);
|
||||
await showReport();
|
||||
await page.click('text="is a test"');
|
||||
await expect(page.locator('.test-error-view').getByText('old')).toHaveCSS('text-decoration', 'line-through solid rgb(205, 49, 49)');
|
||||
await expect(page.locator('.test-error-view').getByText('new', { exact: true })).toHaveCSS('text-decoration', 'none solid rgb(0, 188, 0)');
|
||||
await expect(page.locator('.test-error-view').getByText('common Expected:')).toHaveCSS('text-decoration', 'none solid rgb(36, 41, 47)');
|
||||
await expect(page.locator('.test-error-view').getByText('-old')).toHaveCSS('color', 'rgb(205, 49, 49)');
|
||||
await expect(page.locator('.test-error-view').getByText('+new', { exact: true })).toHaveCSS('color', 'rgb(0, 188, 0)');
|
||||
});
|
||||
|
||||
test('should highlight inline textual diff in toHaveText', async ({ runInlineTest, showReport, page }) => {
|
||||
|
|
|
|||
|
|
@ -1393,3 +1393,80 @@ test('should trim+sanitize attachment names and paths', async ({ runInlineTest }
|
|||
]);
|
||||
});
|
||||
|
||||
test.describe('update-snapshots', () => {
|
||||
test('should rebase non-matching image', async ({ runInlineTest }) => {
|
||||
const BAD_PIXELS = 10;
|
||||
const EXPECTED_SNAPSHOT = paintBlackPixels(whiteImage, BAD_PIXELS);
|
||||
|
||||
const result = await runInlineTest({
|
||||
...playwrightConfig({
|
||||
snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}',
|
||||
}),
|
||||
'__screenshots__/a.spec.js/snapshot.png': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test, expect } = require('@playwright/test');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': 'changed' });
|
||||
expect(result.exitCode).toBe(0);
|
||||
const newBaseline = fs.readFileSync(test.info().outputPath('__screenshots__/a.spec.js/snapshot.png'));
|
||||
expect(comparePNGs(newBaseline, whiteImage)).toBe(null);
|
||||
expect(comparePNGs(newBaseline, EXPECTED_SNAPSHOT)).not.toBe(null);
|
||||
});
|
||||
|
||||
test('should not rebase matching image', async ({ runInlineTest }) => {
|
||||
const BAD_PIXELS = 10;
|
||||
const EXPECTED_SNAPSHOT = paintBlackPixels(whiteImage, BAD_PIXELS);
|
||||
|
||||
const result = await runInlineTest({
|
||||
...playwrightConfig({
|
||||
snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}',
|
||||
expect: {
|
||||
toHaveScreenshot: {
|
||||
maxDiffPixels: BAD_PIXELS
|
||||
}
|
||||
}
|
||||
}),
|
||||
'__screenshots__/a.spec.js/snapshot.png': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test, expect } = require('@playwright/test');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': 'changed' });
|
||||
expect(result.exitCode).toBe(0);
|
||||
const newBaseline = fs.readFileSync(test.info().outputPath('__screenshots__/a.spec.js/snapshot.png'));
|
||||
expect(comparePNGs(newBaseline, EXPECTED_SNAPSHOT)).toBe(null);
|
||||
expect(comparePNGs(newBaseline, whiteImage)).not.toBe(null);
|
||||
});
|
||||
|
||||
test('should rebase matching image with update-snapshots=all', async ({ runInlineTest }) => {
|
||||
const BAD_PIXELS = 10;
|
||||
const EXPECTED_SNAPSHOT = paintBlackPixels(whiteImage, BAD_PIXELS);
|
||||
|
||||
const result = await runInlineTest({
|
||||
...playwrightConfig({
|
||||
snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}',
|
||||
expect: {
|
||||
toHaveScreenshot: {
|
||||
maxDiffPixels: BAD_PIXELS
|
||||
}
|
||||
}
|
||||
}),
|
||||
'__screenshots__/a.spec.js/snapshot.png': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test, expect } = require('@playwright/test');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': 'all' });
|
||||
expect(result.exitCode).toBe(0);
|
||||
const newBaseline = fs.readFileSync(test.info().outputPath('__screenshots__/a.spec.js/snapshot.png'));
|
||||
expect(comparePNGs(newBaseline, whiteImage)).toBe(null);
|
||||
expect(comparePNGs(newBaseline, EXPECTED_SNAPSHOT)).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ function trimPatch(patch: string) {
|
|||
|
||||
test('should update snapshot with the update-snapshots flag with multiple projects', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'.git/marker': '',
|
||||
'playwright.config.ts': `
|
||||
export default { projects: [{ name: 'p1' }, { name: 'p2' }] };
|
||||
`,
|
||||
|
|
@ -73,6 +74,7 @@ test('should update snapshot with the update-snapshots flag with multiple projec
|
|||
|
||||
test('should update missing snapshots', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'.git/marker': '',
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
|
|
@ -116,6 +118,7 @@ test('should update missing snapshots', async ({ runInlineTest }, testInfo) => {
|
|||
|
||||
test('should generate baseline with regex', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'.git/marker': '',
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
|
|
@ -172,6 +175,7 @@ test('should generate baseline with regex', async ({ runInlineTest }, testInfo)
|
|||
|
||||
test('should generate baseline with special characters', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'.git/marker': '',
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
|
|
@ -241,6 +245,7 @@ test('should generate baseline with special characters', async ({ runInlineTest
|
|||
|
||||
test('should update missing snapshots in tsx', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'.git/marker': '',
|
||||
'playwright.config.ts': playwrightCtConfigText,
|
||||
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
|
||||
'playwright/index.ts': ``,
|
||||
|
|
@ -286,6 +291,7 @@ test('should update missing snapshots in tsx', async ({ runInlineTest }, testInf
|
|||
|
||||
test('should update multiple files', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'.git/marker': '',
|
||||
'playwright.config.ts': playwrightCtConfigText,
|
||||
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
|
||||
'playwright/index.ts': ``,
|
||||
|
|
@ -365,6 +371,7 @@ diff --git a/src/button-2.test.tsx b/src/button-2.test.tsx
|
|||
|
||||
test('should generate baseline for input values', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'.git/marker': '',
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
|
|
@ -400,6 +407,7 @@ test('should generate baseline for input values', async ({ runInlineTest }, test
|
|||
|
||||
test('should not update snapshots when locator did not match', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'.git/marker': '',
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
|
|
@ -416,3 +424,148 @@ test('should not update snapshots when locator did not match', async ({ runInlin
|
|||
expect(result.output).toContain('Expected: "- heading"');
|
||||
expect(result.output).toContain('Received: <element not found>');
|
||||
});
|
||||
|
||||
test.describe('update-snapshots none', () => {
|
||||
test('should create new baseline for matching snapshot', 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><h1>world</h1>\`);
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot(\`\`);
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': 'none' });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
||||
expect(fs.existsSync(patchPath)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('update-snapshots all', () => {
|
||||
test('should create new baseline for matching snapshot', 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><h1>world</h1>\`);
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||
- heading "hello"
|
||||
\`);
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': 'all' });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
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
|
||||
@@ -3,7 +3,8 @@
|
||||
test('test', async ({ page }) => {
|
||||
await page.setContent(\`<h1>hello</h1><h1>world</h1>\`);
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||
- - heading "hello"
|
||||
+ - heading "hello" [level=1]
|
||||
+ - heading "world" [level=1]
|
||||
\`);
|
||||
});
|
||||
|
||||
\\ No newline at end of file
|
||||
`);
|
||||
|
||||
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for:
|
||||
|
||||
a.spec.ts
|
||||
|
||||
git apply test-results/rebaselines.patch
|
||||
`);
|
||||
|
||||
execSync(`patch -p1 < ${patchPath}`, { cwd: testInfo.outputPath() });
|
||||
const result2 = await runInlineTest({});
|
||||
expect(result2.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('update-source-method', () => {
|
||||
test('should overwrite source', 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(\`
|
||||
- heading "world"
|
||||
\`);
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': 'all', 'update-source-method': 'overwrite' });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
||||
expect(fs.existsSync(patchPath)).toBeFalsy();
|
||||
|
||||
const data = fs.readFileSync(testInfo.outputPath('a.spec.ts'), 'utf-8');
|
||||
expect(data).toBe(`
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
await page.setContent(\`<h1>hello</h1>\`);
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||
- heading "hello" [level=1]
|
||||
\`);
|
||||
});
|
||||
`);
|
||||
|
||||
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for:
|
||||
|
||||
a.spec.ts
|
||||
`);
|
||||
|
||||
const result2 = await runInlineTest({});
|
||||
expect(result2.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should 3way source', 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(\`
|
||||
- heading "world"
|
||||
\`);
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': 'all', 'update-source-method': '3way' });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
|
||||
expect(fs.existsSync(patchPath)).toBeFalsy();
|
||||
|
||||
const data = fs.readFileSync(testInfo.outputPath('a.spec.ts'), 'utf-8');
|
||||
expect(data).toBe(`
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
await page.setContent(\`<h1>hello</h1>\`);
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||
\<<<<<<< HEAD
|
||||
- heading "world"
|
||||
=======
|
||||
- heading "hello" [level=1]
|
||||
>>>>>>> SNAPSHOT
|
||||
\`);
|
||||
});
|
||||
`);
|
||||
|
||||
expect(stripAnsi(result.output).replace(/\\/g, '/')).toContain(`New baselines created for:
|
||||
|
||||
a.spec.ts
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export const webView2Test = baseTest.extend<TraceViewerFixtures>(traceViewerFixt
|
|||
isElectron: [false, { scope: 'worker' }],
|
||||
electronMajorVersion: [0, { scope: 'worker' }],
|
||||
isWebView2: [true, { scope: 'worker' }],
|
||||
isHeadlessShell: [false, { scope: 'worker' }],
|
||||
|
||||
browser: [async ({ playwright }, use, testInfo) => {
|
||||
const cdpPort = 10000 + testInfo.workerIndex;
|
||||
|
|
|
|||
2
utils/generate_types/overrides-test.d.ts
vendored
2
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -65,7 +65,7 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
|
|||
|
||||
export type TestStatus = 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted';
|
||||
|
||||
type TestDetailsAnnotation = {
|
||||
export type TestDetailsAnnotation = {
|
||||
type: string;
|
||||
description?: string;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue