Compare commits
27 commits
main
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f359b9840 | ||
|
|
88e0a3b72c | ||
|
|
4eb6fca149 | ||
|
|
e55a3ffbe0 | ||
|
|
18de7bd61d | ||
|
|
cfa7f13e43 | ||
|
|
8e98b44b96 | ||
|
|
0093e8a753 | ||
|
|
c090a55828 | ||
|
|
1b0087ed38 | ||
|
|
ef40d21945 | ||
|
|
39abfd6481 | ||
|
|
f2bdeb29ef | ||
|
|
78f8852f7d | ||
|
|
88eba741ad | ||
|
|
3dff476a92 | ||
|
|
cd07b45809 | ||
|
|
16c4ae0831 | ||
|
|
eab96de6e4 | ||
|
|
f9ea3d53bc | ||
|
|
cf1a60bce8 | ||
|
|
4897967780 | ||
|
|
97bca5c431 | ||
|
|
b728609592 | ||
|
|
2531c590bc | ||
|
|
503dc782b6 | ||
|
|
84f36df415 |
23
.github/workflows/tests_primary.yml
vendored
23
.github/workflows/tests_primary.yml
vendored
|
|
@ -56,9 +56,6 @@ jobs:
|
|||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
node-version: [12]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
node-version: 16
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
@ -78,6 +75,26 @@ jobs:
|
|||
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
|
||||
if: always()
|
||||
|
||||
test_test_runner_esm:
|
||||
name: Test Runner ESM
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm i -g npm@8.3
|
||||
- run: npm ci
|
||||
env:
|
||||
DEBUG: pw:install
|
||||
- run: npm run build
|
||||
- run: npx playwright install --with-deps
|
||||
- run: npm run ttest -- esm.spec.ts
|
||||
|
||||
test_html_report:
|
||||
name: HTML Report
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@ Waits for the specific [`param: selector`] to either appear or disappear, depend
|
|||
Selector to wait for.
|
||||
|
||||
### option: AndroidDevice.wait.state
|
||||
- `state` <"gone">
|
||||
- `state` <[AndroidDeviceState]<"gone">>
|
||||
|
||||
Optional state. Can be either:
|
||||
* default - wait for element to be present.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# class: APIRequest
|
||||
* langs: js, java, python
|
||||
|
||||
Exposes API that can be used for the Web API testing. Each Playwright browser context
|
||||
has a APIRequestContext instance attached which shares cookies with the page context.
|
||||
Its also possible to create a new APIRequestContext instance manually. For more information
|
||||
see [here](./class-apirequestcontext).
|
||||
Exposes API that can be used for the Web API testing. This class is used for creating
|
||||
[APIRequestContext] instance which in turn can be used for sending web requests. An instance
|
||||
of this class can be obtained via [`property: Playwright.request`]. For more information
|
||||
see [APIRequestContext].
|
||||
|
||||
## async method: APIRequest.newContext
|
||||
* langs: js, java, python
|
||||
|
|
|
|||
|
|
@ -2,9 +2,23 @@
|
|||
* langs: js, java, python
|
||||
|
||||
This API is used for the Web API testing. You can use it to trigger API endpoints, configure micro-services, prepare
|
||||
environment or the service to your e2e test. When used on [Page] or a [BrowserContext], this API will automatically use
|
||||
the cookies from the corresponding [BrowserContext]. This means that if you log in using this API, your e2e test
|
||||
will be logged in and vice versa.
|
||||
environment or the service to your e2e test.
|
||||
|
||||
Each Playwright browser context has associated with it [APIRequestContext] instance which shares cookie storage with
|
||||
the browser context and can be accessed via [`property: BrowserContext.request`] or [`property: Page.request`].
|
||||
It is also possible to create a new APIRequestContext instance manually by calling [`method: APIRequest.newContext`].
|
||||
|
||||
**Cookie management**
|
||||
|
||||
[APIRequestContext] retuned by [`property: BrowserContext.request`] and [`property: Page.request`] shares cookie
|
||||
storage with the corresponding [BrowserContext]. Each API request will have `Cookie` header populated with the
|
||||
values from the browser context. If the API response contains `Set-Cookie` header it will automatically update
|
||||
[BrowserContext] cookies and requests made from the page will pick them up. This means that if you log in using
|
||||
this API, your e2e test will be logged in and vice versa.
|
||||
|
||||
If you want API requests to not interfere with the browser cookies you shoud create a new [APIRequestContext] by
|
||||
calling [`method: APIRequest.newContext`]. Such `APIRequestContext` object will have its own isolated cookie
|
||||
storage.
|
||||
|
||||
```python async
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -805,7 +805,6 @@ A permission or an array of permissions to grant. Permissions can be one of the
|
|||
* `'midi'`
|
||||
* `'midi-sysex'` (system-exclusive midi)
|
||||
* `'notifications'`
|
||||
* `'push'`
|
||||
* `'camera'`
|
||||
* `'microphone'`
|
||||
* `'background-sync'`
|
||||
|
|
|
|||
|
|
@ -998,36 +998,6 @@ Property value.
|
|||
### option: LocatorAssertions.toHaveJSProperty.timeout = %%-js-assertions-timeout-%%
|
||||
### option: LocatorAssertions.toHaveJSProperty.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
|
||||
## async method: LocatorAssertions.toHaveScreenshot
|
||||
* langs: js
|
||||
|
||||
Ensures that [Locator] resolves to a given screenshot. This function will re-take
|
||||
screenshots until it matches with the saved expectation.
|
||||
|
||||
If there's no expectation yet, it will wait until two consecutive screenshots
|
||||
yield the same result, and save the last one as an expectation.
|
||||
|
||||
```js
|
||||
const locator = page.locator('button');
|
||||
await expect(locator).toHaveScreenshot();
|
||||
```
|
||||
|
||||
### option: LocatorAssertions.toHaveScreenshot.timeout = %%-js-assertions-timeout-%%
|
||||
### option: LocatorAssertions.toHaveScreenshot.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
|
||||
### option: LocatorAssertions.toHaveScreenshot.animations = %%-screenshot-option-animations-%%
|
||||
|
||||
### option: LocatorAssertions.toHaveScreenshot.omitBackground = %%-screenshot-option-omit-background-%%
|
||||
|
||||
### option: LocatorAssertions.toHaveScreenshot.mask = %%-screenshot-option-mask-%%
|
||||
|
||||
### option: LocatorAssertions.toHaveScreenshot.maxDiffPixels = %%-assertions-max-diff-pixels-%%
|
||||
|
||||
### option: LocatorAssertions.toHaveScreenshot.maxDiffPixelRatio = %%-assertions-max-diff-pixel-ratio-%%
|
||||
|
||||
### option: LocatorAssertions.toHaveScreenshot.threshold = %%-assertions-threshold-%%
|
||||
|
||||
|
||||
## async method: LocatorAssertions.toHaveText
|
||||
* langs:
|
||||
- alias-java: hasText
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ Determines whether sink is interested in the logger with the given name and seve
|
|||
logger name
|
||||
|
||||
### param: Logger.isEnabled.severity
|
||||
- `severity` <"verbose"|"info"|"warning"|"error">
|
||||
- `severity` <[LogSeverity]<"verbose"|"info"|"warning"|"error">>
|
||||
|
||||
## method: Logger.log
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ logger name
|
|||
logger name
|
||||
|
||||
### param: Logger.log.severity
|
||||
- `severity` <"verbose"|"info"|"warning"|"error">
|
||||
- `severity` <[LogSeverity]<"verbose"|"info"|"warning"|"error">>
|
||||
|
||||
### param: Logger.log.message
|
||||
- `message` <[string]|[Error]>
|
||||
|
|
|
|||
|
|
@ -2475,7 +2475,8 @@ last redirect.
|
|||
* langs: js, java, python
|
||||
- type: <[APIRequestContext]>
|
||||
|
||||
API testing helper associated with this page. Requests made with this API will use page cookies.
|
||||
API testing helper associated with this page. This method returns the same instance as
|
||||
[`property: BrowserContext.request`] on the page's context. See [`property: BrowserContext.request`] for more details.
|
||||
|
||||
## async method: Page.route
|
||||
|
||||
|
|
|
|||
|
|
@ -114,38 +114,6 @@ Expected substring or RegExp.
|
|||
### option: PageAssertions.NotToHaveURL.timeout = %%-js-assertions-timeout-%%
|
||||
### option: PageAssertions.NotToHaveURL.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
|
||||
## async method: PageAssertions.toHaveScreenshot
|
||||
* langs: js
|
||||
|
||||
Ensures that the page resolves to a given screenshot. This function will re-take
|
||||
screenshots until it matches with the saved expectation.
|
||||
|
||||
If there's no expectation yet, it will wait until two consecutive screenshots
|
||||
yield the same result, and save the last one as an expectation.
|
||||
|
||||
```js
|
||||
await expect(page).toHaveScreenshot();
|
||||
```
|
||||
|
||||
### option: PageAssertions.toHaveScreenshot.timeout = %%-js-assertions-timeout-%%
|
||||
### option: PageAssertions.toHaveScreenshot.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
|
||||
### option: PageAssertions.toHaveScreenshot.animations = %%-screenshot-option-animations-%%
|
||||
|
||||
### option: PageAssertions.toHaveScreenshot.omitBackground = %%-screenshot-option-omit-background-%%
|
||||
|
||||
### option: PageAssertions.toHaveScreenshot.fullPage = %%-screenshot-option-full-page-%%
|
||||
|
||||
### option: PageAssertions.toHaveScreenshot.clip = %%-screenshot-option-clip-%%
|
||||
|
||||
### option: PageAssertions.toHaveScreenshot.mask = %%-screenshot-option-mask-%%
|
||||
|
||||
### option: PageAssertions.toHaveScreenshot.maxDiffPixels = %%-assertions-max-diff-pixels-%%
|
||||
|
||||
### option: PageAssertions.toHaveScreenshot.maxDiffPixelRatio = %%-assertions-max-diff-pixel-ratio-%%
|
||||
|
||||
### option: PageAssertions.toHaveScreenshot.threshold = %%-assertions-threshold-%%
|
||||
|
||||
## async method: PageAssertions.toHaveTitle
|
||||
* langs:
|
||||
- alias-java: hasTitle
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
* langs: js
|
||||
|
||||
Playwright provides methods for comparing page and element screenshots with
|
||||
expected values stored in files. See also [`method: PageAssertions.toHaveScreenshot`] and
|
||||
[`LocatorAssertions.toHaveScreenshot`].
|
||||
expected values stored in files.
|
||||
|
||||
```js
|
||||
expect(screenshot).toMatchSnapshot('landing-page.png');
|
||||
|
|
@ -34,4 +33,8 @@ Learn more about [visual comparisons](./test-snapshots.md).
|
|||
|
||||
Snapshot name.
|
||||
|
||||
### option: ScreenshotAssertions.toMatchSnapshot.maxDiffPixels = %%-assertions-max-diff-pixels-%%
|
||||
|
||||
### option: ScreenshotAssertions.toMatchSnapshot.maxDiffPixelRatio = %%-assertions-max-diff-pixel-ratio-%%
|
||||
|
||||
### option: ScreenshotAssertions.toMatchSnapshot.threshold = %%-assertions-threshold-%%
|
||||
|
|
|
|||
|
|
@ -902,7 +902,7 @@ Note that outer and inner locators must belong to the same frame. Inner locator
|
|||
- %%-locator-option-has-%%
|
||||
|
||||
## screenshot-option-animations
|
||||
- `animations` <"disabled">
|
||||
- `animations` <[ScreenshotAnimations]<"disabled">>
|
||||
|
||||
When set to `"disabled"`, stops CSS animations, CSS transitions and Web Animations. Animations get different treatment depending on their duration:
|
||||
* finite animations are fast-forwarded to completion, so they'll fire `transitionend` event.
|
||||
|
|
|
|||
|
|
@ -13,23 +13,7 @@ This image is published on [Docker Hub].
|
|||
|
||||
### Pull the image
|
||||
|
||||
```bash js
|
||||
docker pull mcr.microsoft.com/playwright:focal
|
||||
```
|
||||
|
||||
```bash python
|
||||
docker pull mcr.microsoft.com/playwright/python:focal
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
docker pull mcr.microsoft.com/playwright:focal
|
||||
```
|
||||
|
||||
```bash java
|
||||
docker pull mcr.microsoft.com/playwright/java:focal
|
||||
```
|
||||
|
||||
or pinned to a specific Playwright version (recommended). Replace 1.20.0 with your Playwright version:
|
||||
Replace 1.20.0 with your Playwright version:
|
||||
|
||||
```bash js
|
||||
docker pull mcr.microsoft.com/playwright:v1.20.0-focal
|
||||
|
|
@ -56,19 +40,19 @@ By default, the Docker image will use the `root` user to run the browsers. This
|
|||
On trusted websites, you can avoid creating a separate user and use root for it since you trust the code which will run on the browsers.
|
||||
|
||||
```bash js
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright:focal /bin/bash
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright:v1.20.0-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash python
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/python:focal /bin/bash
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/python:v1.20.0-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright:focal /bin/bash
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright:v1.20.0-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash java
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/java:focal /bin/bash
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/java:v1.20.0-focal /bin/bash
|
||||
```
|
||||
|
||||
#### Crawling and scraping
|
||||
|
|
@ -76,19 +60,19 @@ docker run -it --rm --ipc=host mcr.microsoft.com/playwright/java:focal /bin/bash
|
|||
On untrusted websites, it's recommended to use a separate user for launching the browsers in combination with the seccomp profile. Inside the container or if you are using the Docker image as a base image you have to use `adduser` for it.
|
||||
|
||||
```bash js
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright:focal /bin/bash
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright:v1.20.0-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash python
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/python:focal /bin/bash
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/python:v1.20.0-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright:focal /bin/bash
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright:v1.20.0-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash java
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/java:focal /bin/bash
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/java:v1.20.0-focal /bin/bash
|
||||
```
|
||||
|
||||
[`seccomp_profile.json`](https://github.com/microsoft/playwright/blob/main/utils/docker/seccomp_profile.json) is needed to run Chromium with sandbox. This is a [default Docker seccomp profile](https://github.com/docker/engine/blob/d0d99b04cf6e00ed3fc27e81fc3d94e7eda70af3/profiles/seccomp/default.json) with extra user namespace cloning permissions:
|
||||
|
|
@ -127,7 +111,6 @@ Docker images are published automatically by GitHub Actions. We currently publis
|
|||
following tags (`v1.20.0` in this case is an example:):
|
||||
- `:next` - tip-of-tree image version based on Ubuntu 20.04 LTS (Focal Fossa).
|
||||
- `:next-focal` - tip-of-tree image version based on Ubuntu 20.04 LTS (Focal Fossa).
|
||||
- `:focal` - last Playwright release docker image based on Ubuntu 20.04 LTS (Focal Fossa).
|
||||
- `:v1.20.0` - Playwright v1.20.0 release docker image based on Ubuntu 20.04 LTS (Focal Fossa).
|
||||
- `:v1.20.0-focal` - Playwright v1.20.0 release docker image based on Ubuntu 20.04 LTS (Focal Fossa).
|
||||
- `:sha-XXXXXXX` - docker image for every commit that changed
|
||||
|
|
|
|||
|
|
@ -5,6 +5,61 @@ title: "Release notes"
|
|||
|
||||
<!-- TOC -->
|
||||
|
||||
## Version 1.20
|
||||
|
||||
### Web-First Assertions
|
||||
|
||||
Playwright for .NET 1.20 introduces [Web-First Assertions](./test-assertions).
|
||||
|
||||
Consider the following example:
|
||||
|
||||
```csharp
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Playwright.NUnit;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Playwright.TestingHarnessTest.NUnit
|
||||
{
|
||||
public class ExampleTests : PageTest
|
||||
{
|
||||
[Test]
|
||||
public async Task StatusBecomesSubmitted()
|
||||
{
|
||||
await Expect(Page.Locator(".status")).ToHaveTextAsync("Submitted");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Playwright will be re-testing the node with the selector `.status` until
|
||||
fetched Node has the `"Submitted"` text. It will be re-fetching the node and
|
||||
checking it over and over, until the condition is met or until the timeout is
|
||||
reached. You can pass this timeout as an option.
|
||||
|
||||
Read more in [our documentation](./test-assertions).
|
||||
|
||||
### Other Updates
|
||||
|
||||
- New options for methods [`method: Page.screenshot`], [`method: Locator.screenshot`] and [`method: ElementHandle.screenshot`]:
|
||||
* Option `ScreenshotAnimations.Disabled` rewinds all CSS animations and transitions to a consistent state
|
||||
* Option `mask: Locator[]` masks given elements, overlaying them with pink `#FF00FF` boxes.
|
||||
- [`method: Locator.highlight`] visually reveals element(s) for easier debugging.
|
||||
|
||||
### Announcements
|
||||
|
||||
- v1.20 is the last release to receive WebKit update for macOS 10.15 Catalina. Please update MacOS to keep using latest & greatest WebKit!
|
||||
|
||||
### Browser Versions
|
||||
|
||||
- Chromium 101.0.4921.0
|
||||
- Mozilla Firefox 97.0.1
|
||||
- WebKit 15.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
- Google Chrome 99
|
||||
- Microsoft Edge 99
|
||||
|
||||
## Version 1.19
|
||||
|
||||
### Highlights
|
||||
|
|
|
|||
|
|
@ -5,6 +5,32 @@ title: "Release notes"
|
|||
|
||||
<!-- TOC -->
|
||||
|
||||
## Version 1.20
|
||||
|
||||
### Highlights
|
||||
|
||||
- New options for methods [`method: Page.screenshot`], [`method: Locator.screenshot`] and [`method: ElementHandle.screenshot`]:
|
||||
* Option `ScreenshotAnimations.DISABLED` rewinds all CSS animations and transitions to a consistent state
|
||||
* Option `mask: Locator[]` masks given elements, overlaying them with pink `#FF00FF` boxes.
|
||||
- [Trace Viewer](./trace-viewer) now shows [API testing requests](./api-testing).
|
||||
- [`method: Locator.highlight`] visually reveals element(s) for easier debugging.
|
||||
|
||||
### Announcements
|
||||
|
||||
- v1.20 is the last release to receive WebKit update for macOS 10.15 Catalina. Please update MacOS to keep using latest & greatest WebKit!
|
||||
|
||||
### Browser Versions
|
||||
|
||||
- Chromium 101.0.4921.0
|
||||
- Mozilla Firefox 97.0.1
|
||||
- WebKit 15.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
- Google Chrome 99
|
||||
- Microsoft Edge 99
|
||||
|
||||
|
||||
## Version 1.19
|
||||
|
||||
### Highlights
|
||||
|
|
@ -54,7 +80,7 @@ Read more about it in our [API testing guide](./api-testing).
|
|||
|
||||
### Web-First Assertions
|
||||
|
||||
Playwright for Java 1.18 introduces [Web-First Assertions](./api/class-playwrightassertions).
|
||||
Playwright for Java 1.18 introduces [Web-First Assertions](./test-assertions).
|
||||
|
||||
Consider the following example:
|
||||
|
||||
|
|
@ -78,7 +104,7 @@ fetched Node has the `"Submitted"` text. It will be re-fetching the node and
|
|||
checking it over and over, until the condition is met or until the timeout is
|
||||
reached. You can pass this timeout as an option.
|
||||
|
||||
Read more in [our documentation](./api/class-playwrightassertions).
|
||||
Read more in [our documentation](./test-assertions).
|
||||
|
||||
### Locator Improvements
|
||||
|
||||
|
|
|
|||
|
|
@ -7,28 +7,21 @@ title: "Release notes"
|
|||
|
||||
## Version 1.20
|
||||
|
||||
### Visual Regression Testing
|
||||
### Highlights
|
||||
|
||||
- New options for methods [`method: Page.screenshot`], [`method: Locator.screenshot`] and [`method: ElementHandle.screenshot`]:
|
||||
* Option `animations: "disabled"` rewinds all CSS animations and transitions to a consistent state
|
||||
* Option `mask: Locator[]` masks given elements, overlaying them with pink `#FF00FF` boxes.
|
||||
- New web-first assertions for screenshots: [`method: PageAssertions.toHaveScreenshot`] and [`method: LocatorAssertions.toHaveScreenshot`]. These methods will re-take screenshot until it matches the saved expectation. When generating a new expectation, the method will re-take screenshots until 2 consecutive screenshots match.
|
||||
|
||||
New methods support both named and anonymous (auto-named) expectations:
|
||||
- `expect().toMatchSnapshot()` now supports anonymous snapshots: when snapshot name is missing, Playwright Test will generate one
|
||||
automatically:
|
||||
|
||||
```js
|
||||
// Take a full-page screenshot with a named expectation `fullpage.png`.
|
||||
await expect(page).toHaveScreenshot('fullpage.png', { fullPage: true });
|
||||
// Take a screenshot of an element with anonymous expectation.
|
||||
await expect(page.locator('text=Booking')).toHaveScreenshot();
|
||||
expect('Web is Awesome <3').toMatchSnapshot();
|
||||
```
|
||||
|
||||
Methods support all screenshot options from [`method: Page.screenshot`] and [`method: Locator.screenshot`].
|
||||
|
||||
These methods also support new `maxDiffPixels` and `maxDiffPixelRatio` options for fine-grained screenshot comparison:
|
||||
- New `maxDiffPixels` and `maxDiffPixelRatio` options for fine-grained screenshot comparison using `expect().toMatchSnapshot()`:
|
||||
|
||||
```js
|
||||
await expect(page).toHaveScreenshot({
|
||||
expect(await page.screenshot()).toMatchSnapshot({
|
||||
fullPage: true, // take a full page screenshot
|
||||
maxDiffPixels: 27, // allow no more than 27 different pixels.
|
||||
});
|
||||
|
|
@ -36,8 +29,6 @@ title: "Release notes"
|
|||
|
||||
It is most convenient to specify `maxDiffPixels` or `maxDiffPixelRatio` once in [`property: TestConfig.expect`].
|
||||
|
||||
### Other Updates
|
||||
|
||||
- Playwright Test now adds [`property: TestConfig.fullyParallel`] mode. By default, Playwright Test parallelizes between files. In fully parallel mode, tests inside a single file are also run in parallel. You can also use `--fully-parallel` command line flag.
|
||||
|
||||
```ts
|
||||
|
|
@ -55,28 +46,20 @@ title: "Release notes"
|
|||
projects: [
|
||||
{
|
||||
name: 'smoke tests',
|
||||
grep: '@smoke',
|
||||
grep: /@smoke/,
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
- [Trace Viewer](./trace-viewer) now shows [API testing requests](./src/test-api-testing).
|
||||
- `expect().toMatchSnapshot()` now supports anonymous snapshots: when snapshot name is missing, Playwright Test will generate one
|
||||
automatically:
|
||||
|
||||
```js
|
||||
expect('Web is Awesome <3').toMatchSnapshot();
|
||||
```
|
||||
|
||||
- [Trace Viewer](./trace-viewer) now shows [API testing requests](./test-api-testing).
|
||||
- [`method: Locator.highlight`] visually reveals element(s) for easier debugging.
|
||||
|
||||
### Announcements
|
||||
|
||||
- We now ship a designated Python docker image `mcr.microsoft.com/playwright/python`. Please switch over to it if you use
|
||||
Python. This is the last release that includes Python inside our javascript `mcr.microsoft.com/playwright` docker image.
|
||||
- v1.20 is the last release that ships WebKit for macOS 10.15 Catalina. All future versions will support WebKit for macOS 11 BigSur
|
||||
and up.
|
||||
- v1.20 is the last release to receive WebKit update for macOS 10.15 Catalina. Please update MacOS to keep using latest & greatest WebKit!
|
||||
|
||||
### Browser Versions
|
||||
|
||||
|
|
@ -178,7 +161,7 @@ This version was also tested against the following stable channels:
|
|||
### Locator Improvements
|
||||
|
||||
- [`method: Locator.dragTo`]
|
||||
- [`expect(locator).toBeChecked({ checked })`](./api/class-playwrightassertions#locator-assertions-to-be-checked)
|
||||
- [`expect(locator).toBeChecked({ checked })`](./test-assertions#locator-assertions-to-be-checked)
|
||||
- Each locator can now be optionally filtered by the text it contains:
|
||||
```js
|
||||
await page.locator('li', { hasText: 'my item' }).locator('button').click();
|
||||
|
|
@ -188,7 +171,7 @@ This version was also tested against the following stable channels:
|
|||
|
||||
### Testing API improvements
|
||||
|
||||
- [`expect(response).toBeOK()`](./api/class-playwrightassertions)
|
||||
- [`expect(response).toBeOK()`](./test-assertions)
|
||||
- [`testInfo.attach()`](./api/class-testinfo#test-info-attach)
|
||||
- [`test.info()`](./api/class-test#test-info)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,33 @@ title: "Release notes"
|
|||
|
||||
<!-- TOC -->
|
||||
|
||||
## Version 1.20
|
||||
|
||||
### Highlights
|
||||
|
||||
- New options for methods [`method: Page.screenshot`], [`method: Locator.screenshot`] and [`method: ElementHandle.screenshot`]:
|
||||
* Option `animations: "disabled"` rewinds all CSS animations and transitions to a consistent state
|
||||
* Option `mask: Locator[]` masks given elements, overlaying them with pink `#FF00FF` boxes.
|
||||
- [Trace Viewer](./trace-viewer) now shows [API testing requests](./api-testing).
|
||||
- [`method: Locator.highlight`] visually reveals element(s) for easier debugging.
|
||||
|
||||
### Announcements
|
||||
|
||||
- We now ship a designated Python docker image `mcr.microsoft.com/playwright/python`. Please switch over to it if you use
|
||||
Python. This is the last release that includes Python inside our javascript `mcr.microsoft.com/playwright` docker image.
|
||||
- v1.20 is the last release to receive WebKit update for macOS 10.15 Catalina. Please update MacOS to keep using latest & greatest WebKit!
|
||||
|
||||
### Browser Versions
|
||||
|
||||
- Chromium 101.0.4921.0
|
||||
- Mozilla Firefox 97.0.1
|
||||
- WebKit 15.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
- Google Chrome 99
|
||||
- Microsoft Edge 99
|
||||
|
||||
## Version 1.19
|
||||
|
||||
### Highlights
|
||||
|
|
@ -64,7 +91,7 @@ Read more in [our documentation](./api/class-apirequestcontext).
|
|||
|
||||
### Web-First Assertions
|
||||
|
||||
Playwright for Python 1.18 introduces [Web-First Assertions](./api/class-playwrightassertions).
|
||||
Playwright for Python 1.18 introduces [Web-First Assertions](./test-assertions).
|
||||
|
||||
Consider the following example:
|
||||
|
||||
|
|
@ -91,7 +118,7 @@ fetched Node has the `"Submitted"` text. It will be re-fetching the node and
|
|||
checking it over and over, until the condition is met or until the timeout is
|
||||
reached. You can pass this timeout as an option.
|
||||
|
||||
Read more in [our documentation](./api/class-playwrightassertions).
|
||||
Read more in [our documentation](./test-assertions).
|
||||
|
||||
### Locator Improvements
|
||||
|
||||
|
|
|
|||
|
|
@ -387,3 +387,73 @@ await requestContext.storageState({ path: 'state.json' });
|
|||
// Create a new context with the saved storage state.
|
||||
const context = await browser.newContext({ storageState: 'state.json' });
|
||||
```
|
||||
|
||||
## Context request vs global request
|
||||
|
||||
There are two types of [APIRequestContext]:
|
||||
* associated with a [BrowserContext]
|
||||
* isolated instance, created via [`method: APIRequest.newContext`]
|
||||
|
||||
The main difference is that [APIRequestConxtext] accessible via [`property: BrowserContext.request`] and
|
||||
[`property: Page.request`] will populate request's `Cookie` header from the browser context and will
|
||||
automatically update browser cookies if [APIResponse] has `Set-Cookie` header:
|
||||
|
||||
```js
|
||||
test('context request will share cookie storage with its browser context', async ({ page, context }) => {
|
||||
await context.route('https://www.github.com/', async (route) => {
|
||||
// Send an API request that shares cookie storage with the browser context.
|
||||
const response = await context.request.fetch(route.request());
|
||||
const responseHeaders = response.headers();
|
||||
|
||||
// The response will have 'Set-Cookie' header.
|
||||
const responseCookies = new Map(responseHeaders['set-cookie'].split('\n').map(c => c.split(';', 2)[0].split('=')));
|
||||
// The response will have 3 cookies in 'Set-Cookie' header.
|
||||
expect(responseCookies.size).toBe(3);
|
||||
const contextCookies = await context.cookies();
|
||||
// The browser context will already contain all the cookies from the API response.
|
||||
expect(new Map(contextCookies.map(({name, value}) => [name, value]))).toEqual(responseCookies);
|
||||
|
||||
route.fulfill({
|
||||
response,
|
||||
headers: {...responseHeaders, foo: 'bar'},
|
||||
});
|
||||
});
|
||||
await page.goto('https://www.github.com/');
|
||||
});
|
||||
```
|
||||
|
||||
If you don't want [APIRequestContext] to use and update cookies from the browser context, you can manually
|
||||
create a new instance of [APIRequestContext] which will have its own isolated cookies:
|
||||
|
||||
```js
|
||||
test('global context request has isolated cookie storage', async ({ page, context, browser, playwright }) => {
|
||||
// Create a new instance of APIRequestContext with isolated cookie storage.
|
||||
const request = await playwright.request.newContext();
|
||||
await context.route('https://www.github.com/', async (route) => {
|
||||
const response = await request.fetch(route.request());
|
||||
const responseHeaders = response.headers();
|
||||
|
||||
const responseCookies = new Map(responseHeaders['set-cookie'].split('\n').map(c => c.split(';', 2)[0].split('=')));
|
||||
// The response will have 3 cookies in 'Set-Cookie' header.
|
||||
expect(responseCookies.size).toBe(3);
|
||||
const contextCookies = await context.cookies();
|
||||
// The browser context will not have any cookies from the isolated API request.
|
||||
expect(contextCookies.length).toBe(0);
|
||||
|
||||
// Manually export cookie storage.
|
||||
const storageState = await request.storageState();
|
||||
// Create a new context and initialize it with the cookies from the global request.
|
||||
const browserContext2 = await browser.newContext({ storageState });
|
||||
const contextCookies2 = await browserContext2.cookies();
|
||||
// The new browser context will already contain all the cookies from the API response.
|
||||
expect(new Map(contextCookies2.map(({name, value}) => [name, value]))).toEqual(responseCookies);
|
||||
|
||||
route.fulfill({
|
||||
response,
|
||||
headers: {...responseHeaders, foo: 'bar'},
|
||||
});
|
||||
});
|
||||
await page.goto('https://www.github.com/');
|
||||
await request.dispose();
|
||||
});
|
||||
```
|
||||
|
|
@ -288,7 +288,7 @@ test('runs second', async ({ page }) => {});
|
|||
```
|
||||
|
||||
### option: Test.describe.configure.mode
|
||||
- `mode` <"parallel"|"serial">
|
||||
- `mode` <[TestMode]<"parallel"|"serial">>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -36,12 +36,10 @@ export default config;
|
|||
## property: TestConfig.expect
|
||||
- type: <[Object]>
|
||||
- `timeout` <[int]> Default timeout for async expect matchers in milliseconds, defaults to 5000ms.
|
||||
- `toHaveScreenshot` <[Object]>
|
||||
- `toMatchSnapshot` <[Object]>
|
||||
- `threshold` <[float]> an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`.
|
||||
- `maxDiffPixels` <[int]> an acceptable amount of pixels that could be different, unset by default.
|
||||
- `maxDiffPixelRatio` <[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default.
|
||||
- `toMatchSnapshot` <[Object]>
|
||||
- `threshold` <[float]> an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`.
|
||||
|
||||
Configuration for the `expect` assertion library. Learn more about [various timeouts](./test-timeouts.md).
|
||||
|
||||
|
|
@ -53,7 +51,7 @@ Configuration for the `expect` assertion library. Learn more about [various time
|
|||
const config = {
|
||||
expect: {
|
||||
timeout: 10000,
|
||||
toHaveScreenshot: {
|
||||
toMatchSnapshot: {
|
||||
maxDiffPixels: 10,
|
||||
},
|
||||
},
|
||||
|
|
@ -69,7 +67,7 @@ import { PlaywrightTestConfig } from '@playwright/test';
|
|||
const config: PlaywrightTestConfig = {
|
||||
expect: {
|
||||
timeout: 10000,
|
||||
toHaveScreenshot: {
|
||||
toMatchSnapshot: {
|
||||
maxDiffPixels: 10,
|
||||
},
|
||||
},
|
||||
|
|
@ -301,7 +299,7 @@ test('example test', async ({}, testInfo) => {
|
|||
## property: TestConfig.snapshotDir
|
||||
- type: <[string]>
|
||||
|
||||
The base directory, relative to the config file, for snapshot files created with `toMatchSnapshot` and `toHaveScreenshot`. Defaults to [`property: TestConfig.testDir`].
|
||||
The base directory, relative to the config file, for snapshot files created with `toMatchSnapshot`. Defaults to [`property: TestConfig.testDir`].
|
||||
|
||||
The directory for each test can be accessed by [`property: TestInfo.snapshotDir`] and [`method: TestInfo.snapshotPath`].
|
||||
|
||||
|
|
|
|||
|
|
@ -382,7 +382,7 @@ The name of the snapshot or the path segments to define the snapshot file path.
|
|||
## property: TestInfo.snapshotSuffix
|
||||
- type: <[string]>
|
||||
|
||||
Suffix used to differentiate snapshots between multiple test configurations. For example, if snapshots depend on the platform, you can set `testInfo.snapshotSuffix` equal to `process.platform`. In this case both `expect(value).toMatchSnapshot(snapshotName)` and `expect(page).toHaveScreenshot(snapshotName)` will use different snapshots depending on the platform. Learn more about [snapshots](./test-snapshots.md).
|
||||
Suffix used to differentiate snapshots between multiple test configurations. For example, if snapshots depend on the platform, you can set `testInfo.snapshotSuffix` equal to `process.platform`. In this case both `expect(value).toMatchSnapshot(snapshotName)` will use different snapshots depending on the platform. Learn more about [snapshots](./test-snapshots.md).
|
||||
|
||||
## property: TestInfo.status
|
||||
- type: <[void]|[TestStatus]<"passed"|"failed"|"timedOut"|"skipped">>
|
||||
|
|
|
|||
|
|
@ -107,12 +107,10 @@ export default config;
|
|||
## property: TestProject.expect
|
||||
- type: <[Object]>
|
||||
- `timeout` <[int]> Default timeout for async expect matchers in milliseconds, defaults to 5000ms.
|
||||
- `toHaveScreenshot` <[Object]>
|
||||
- `toMatchSnapshot` <[Object]>
|
||||
- `threshold` <[float]> an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`.
|
||||
- `maxDiffPixels` <[int]> an acceptable amount of pixels that could be different, unset by default.
|
||||
- `maxDiffPixelRatio` <[float]> an acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1` , unset by default.
|
||||
- `toMatchSnapshot` <[Object]>
|
||||
- `threshold` <[float]> an acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between the same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`.
|
||||
|
||||
Configuration for the `expect` assertion library.
|
||||
|
||||
|
|
@ -153,7 +151,7 @@ Project name is visible in the report and during test execution.
|
|||
## property: TestProject.snapshotDir
|
||||
- type: <[string]>
|
||||
|
||||
The base directory, relative to the config file, for snapshot files created with `toMatchSnapshot` and `toHaveScreenshot`. Defaults to [`property: TestProject.testDir`].
|
||||
The base directory, relative to the config file, for snapshot files created with `toMatchSnapshot`. Defaults to [`property: TestProject.testDir`].
|
||||
|
||||
The directory for each test can be accessed by [`property: TestInfo.snapshotDir`] and [`method: TestInfo.snapshotPath`].
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ exports.PlaywrightDevPage = class PlaywrightDevPage {
|
|||
*/
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.getStartedLink = page.locator('text=Get started');
|
||||
this.coreConceptsLink = page.locator('text=Core concepts');
|
||||
this.getStartedLink = page.locator('a', { hasText: 'Get started' });
|
||||
this.gettingStartedHeader = page.locator('h1', { hasText: 'Getting started' });
|
||||
this.pomLink = page.locator('li', { hasText: 'Playwright Test' }).locator('a', { hasText: 'Page Object Model' });
|
||||
this.tocList = page.locator('article ul > li > a');
|
||||
}
|
||||
|
||||
|
|
@ -29,14 +30,12 @@ exports.PlaywrightDevPage = class PlaywrightDevPage {
|
|||
|
||||
async getStarted() {
|
||||
await this.getStartedLink.first().click();
|
||||
await expect(this.coreConceptsLink).toBeVisible();
|
||||
await expect(this.gettingStartedHeader).toBeVisible();
|
||||
}
|
||||
|
||||
async coreConcepts() {
|
||||
async pageObjectModel() {
|
||||
await this.getStarted();
|
||||
await this.page.click('text=Guides');
|
||||
await this.coreConceptsLink.click();
|
||||
await expect(this.page.locator('h1').locator("text=Core concepts")).toBeVisible();
|
||||
await this.pomLink.click();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -48,13 +47,15 @@ import { expect, Locator, Page } from '@playwright/test';
|
|||
export class PlaywrightDevPage {
|
||||
readonly page: Page;
|
||||
readonly getStartedLink: Locator;
|
||||
readonly coreConceptsLink: Locator;
|
||||
readonly gettingStartedHeader: Locator;
|
||||
readonly pomLink: Locator;
|
||||
readonly tocList: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.getStartedLink = page.locator('text=Get started');
|
||||
this.coreConceptsLink = page.locator('text=Core concepts');
|
||||
this.getStartedLink = page.locator('a', { hasText: 'Get started' });
|
||||
this.gettingStartedHeader = page.locator('h1', { hasText: 'Getting started' });
|
||||
this.pomLink = page.locator('li', { hasText: 'Playwright Test' }).locator('a', { hasText: 'Page Object Model' });
|
||||
this.tocList = page.locator('article ul > li > a');
|
||||
}
|
||||
|
||||
|
|
@ -64,14 +65,12 @@ export class PlaywrightDevPage {
|
|||
|
||||
async getStarted() {
|
||||
await this.getStartedLink.first().click();
|
||||
await expect(this.coreConceptsLink).toBeVisible();
|
||||
await expect(this.gettingStartedHeader).toBeVisible();
|
||||
}
|
||||
|
||||
async coreConcepts() {
|
||||
async pageObjectModel() {
|
||||
await this.getStarted();
|
||||
await this.page.click('text=Guides');
|
||||
await this.coreConceptsLink.click();
|
||||
await expect(this.page.locator('h1').locator("text=Core concepts")).toBeVisible();
|
||||
await this.pomLink.click();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -83,27 +82,28 @@ Now we can use the `PlaywrightDevPage` class in our tests.
|
|||
const { test, expect } = require('@playwright/test');
|
||||
const { PlaywrightDevPage } = require('./playwright-dev-page');
|
||||
|
||||
test('Get Started table of contents', async ({ page }) => {
|
||||
test('getting started should contain table of contents', async ({ page }) => {
|
||||
const playwrightDev = new PlaywrightDevPage(page);
|
||||
await playwrightDev.goto();
|
||||
await playwrightDev.getStarted();
|
||||
await expect(playwrightDev.tocList).toHaveText([
|
||||
'Installation',
|
||||
'First test',
|
||||
'Configuration file',
|
||||
'Writing assertions',
|
||||
'Using test fixtures',
|
||||
'Using test hooks',
|
||||
'Learning the command line',
|
||||
'Creating a configuration file',
|
||||
'Release notes',
|
||||
'Command line',
|
||||
'Configure NPM scripts',
|
||||
'Release notes'
|
||||
]);
|
||||
});
|
||||
|
||||
test('Core Concepts table of contents', async ({ page }) => {
|
||||
test('should show Page Object Model article', async ({ page }) => {
|
||||
const playwrightDev = new PlaywrightDevPage(page);
|
||||
await playwrightDev.goto();
|
||||
await playwrightDev.coreConcepts();
|
||||
await expect(playwrightDev.tocList.first()).toHaveText('Browser');
|
||||
await playwrightDev.pageObjectModel();
|
||||
await expect(page.locator('article')).toContainText('Page Object Model is a common pattern');
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -112,26 +112,27 @@ test('Core Concepts table of contents', async ({ page }) => {
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { PlaywrightDevPage } from './playwright-dev-page';
|
||||
|
||||
test('Get Started table of contents', async ({ page }) => {
|
||||
test('getting started should contain table of contents', async ({ page }) => {
|
||||
const playwrightDev = new PlaywrightDevPage(page);
|
||||
await playwrightDev.goto();
|
||||
await playwrightDev.getStarted();
|
||||
await expect(playwrightDev.tocList).toHaveText([
|
||||
'Installation',
|
||||
'First test',
|
||||
'Configuration file',
|
||||
'Writing assertions',
|
||||
'Using test fixtures',
|
||||
'Using test hooks',
|
||||
'Learning the command line',
|
||||
'Creating a configuration file',
|
||||
'Release notes',
|
||||
'Command line',
|
||||
'Configure NPM scripts',
|
||||
'Release notes'
|
||||
]);
|
||||
});
|
||||
|
||||
test('Core Concepts table of contents', async ({ page }) => {
|
||||
test('should show Page Object Model article', async ({ page }) => {
|
||||
const playwrightDev = new PlaywrightDevPage(page);
|
||||
await playwrightDev.goto();
|
||||
await playwrightDev.coreConcepts();
|
||||
await expect(playwrightDev.tocList.first()).toHaveText('Browser');
|
||||
await playwrightDev.pageObjectModel();
|
||||
await expect(page.locator('article')).toContainText('Page Object Model is a common pattern');
|
||||
});
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ id: test-snapshots
|
|||
title: "Visual comparisons"
|
||||
---
|
||||
|
||||
Playwright Test includes the ability to produce and visually compare screenshots using `await expect(pageOrLocator).toHaveScreenshot()`. On first execution, Playwright test will generate reference screenshots. Subsequent runs will compare against the reference.
|
||||
Playwright Test includes the ability to produce and visually compare screenshots using `expect().toMatchSnapshot()`. On first execution, Playwright test will generate reference screenshots. Subsequent runs will compare against the reference.
|
||||
|
||||
```js js-flavor=js
|
||||
// example.spec.js
|
||||
|
|
@ -11,7 +11,7 @@ const { test, expect } = require('@playwright/test');
|
|||
|
||||
test('example test', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev');
|
||||
await expect(page).toHaveScreenshot();
|
||||
expect(await page.screenshot()).toMatchSnapshot();
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ import { test, expect } from '@playwright/test';
|
|||
|
||||
test('example test', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev');
|
||||
await expect(page).toHaveScreenshot();
|
||||
expect(await page.screenshot()).toMatchSnapshot();
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -45,10 +45,10 @@ drwxr-xr-x 3 user group 96 Jun 4 11:46 example.spec.ts-snapshots
|
|||
The snapshot name `example-test-1-chromium-darwin.png` consists of a few parts:
|
||||
- `example-test-1.png` - an auto-generated name of the snapshot. Alternatively you can specify snapshot name as the first argument of the `toMatchSnapshot()` method:
|
||||
```js js-flavor=js
|
||||
await expect(page).toHaveScreenshot('landing.png');
|
||||
expect(await page.screenshot()).toMatchSnapshot('landing.png');
|
||||
```
|
||||
```js js-flavor=ts
|
||||
await expect(page).toHaveScreenshot('landing.png');
|
||||
expect(await page.screenshot()).toMatchSnapshot('landing.png');
|
||||
```
|
||||
|
||||
- `chromium-darwin` - the browser name and the platform. Screenshots differ between browsers and platforms due to different rendering, fonts and more, so you will need different snapshots for them. If you use multiple projects in your [configuration file](./test-configuration.md), project name will be used instead of `chromium`.
|
||||
|
|
@ -67,10 +67,10 @@ Sometimes you need to update the reference screenshot, for example when the page
|
|||
npx playwright test --update-snapshots
|
||||
```
|
||||
|
||||
> Note that `snapshotName` also accepts an array of path segments to the snapshot file such as `await expect(page).toHaveScreenshot(['relative', 'path', 'to', 'snapshot.png'])`.
|
||||
> Note that `snapshotName` also accepts an array of path segments to the snapshot file such as `expect().toMatchSnapshot(['relative', 'path', 'to', 'snapshot.png'])`.
|
||||
> However, this path must stay within the snapshots directory for each test file (i.e. `a.spec.js-snapshots`), otherwise it will throw.
|
||||
|
||||
Playwright Test uses the [pixelmatch](https://github.com/mapbox/pixelmatch) library. You can [pass various options](./test-assertions#expectpageorlocatortohavescreenshot-options) to modify its behavior:
|
||||
You can pass various options to modify image comparison behavior - see [`method: ScreenshotAssertions.toMatchSnapshot`]:
|
||||
|
||||
```js js-flavor=js
|
||||
// example.spec.js
|
||||
|
|
@ -78,7 +78,7 @@ const { test, expect } = require('@playwright/test');
|
|||
|
||||
test('example test', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev');
|
||||
await expect(page).toHaveScreenshot({ maxDiffPixels: 100 });
|
||||
expect(await page.screenshot()).toMatchSnapshot({ maxDiffPixels: 100 });
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ import { test, expect } from '@playwright/test';
|
|||
|
||||
test('example test', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev');
|
||||
await expect(page).toHaveScreenshot({ maxDiffPixels: 100 });
|
||||
expect(await page.screenshot()).toMatchSnapshot({ maxDiffPixels: 100 });
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ If you'd like to share the default value among all the tests in the project, you
|
|||
```js js-flavor=js
|
||||
module.exports = {
|
||||
expect: {
|
||||
toHaveScreenshot: { maxDiffPixels: 100 },
|
||||
toMatchSnapshot: { maxDiffPixels: 100 },
|
||||
},
|
||||
};
|
||||
```
|
||||
|
|
@ -106,7 +106,7 @@ module.exports = {
|
|||
import { PlaywrightTestConfig } from '@playwright/test';
|
||||
const config: PlaywrightTestConfig = {
|
||||
expect: {
|
||||
toHaveScreenshot: { maxDiffPixels: 100 },
|
||||
toMatchSnapshot: { maxDiffPixels: 100 },
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: "Verification"
|
|||
---
|
||||
|
||||
:::caution
|
||||
We recommend [Web-First Assertions](./api/class-playwrightassertions) that automatically retry until the expected condition is met instead. This helps reducing the flakiness of the tests.
|
||||
We recommend [Web-First Assertions](./test-assertions) that automatically retry until the expected condition is met instead. This helps reducing the flakiness of the tests.
|
||||
:::
|
||||
|
||||
<!-- TOC -->
|
||||
|
|
|
|||
64
package-lock.json
generated
64
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "playwright-internal",
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
|
@ -4856,12 +4856,9 @@
|
|||
"optional": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
|
|
@ -5167,7 +5164,8 @@
|
|||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.5",
|
||||
|
|
@ -7299,11 +7297,11 @@
|
|||
},
|
||||
"packages/html-reporter": {},
|
||||
"packages/playwright": {
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.20.0-next"
|
||||
"playwright-core": "1.20.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7313,11 +7311,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-chromium": {
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.20.0-next"
|
||||
"playwright-core": "1.20.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7327,7 +7325,7 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-core": {
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"colors": "1.4.0",
|
||||
|
|
@ -7365,11 +7363,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-firefox": {
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.20.0-next"
|
||||
"playwright-core": "1.20.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7380,7 +7378,7 @@
|
|||
},
|
||||
"packages/playwright-test": {
|
||||
"name": "@playwright/test",
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.16.7",
|
||||
|
|
@ -7406,13 +7404,13 @@
|
|||
"debug": "4.3.3",
|
||||
"expect": "27.2.5",
|
||||
"jest-matcher-utils": "27.2.5",
|
||||
"json5": "2.2.0",
|
||||
"json5": "2.2.1",
|
||||
"mime": "3.0.0",
|
||||
"minimatch": "3.0.4",
|
||||
"ms": "2.1.3",
|
||||
"open": "8.4.0",
|
||||
"pirates": "4.0.4",
|
||||
"playwright-core": "1.20.0-next",
|
||||
"playwright-core": "1.20.2",
|
||||
"rimraf": "3.0.2",
|
||||
"source-map-support": "0.4.18",
|
||||
"stack-utils": "2.0.5",
|
||||
|
|
@ -7455,11 +7453,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-webkit": {
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.20.0-next"
|
||||
"playwright-core": "1.20.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -8221,13 +8219,13 @@
|
|||
"debug": "4.3.3",
|
||||
"expect": "27.2.5",
|
||||
"jest-matcher-utils": "27.2.5",
|
||||
"json5": "2.2.0",
|
||||
"json5": "2.2.1",
|
||||
"mime": "3.0.0",
|
||||
"minimatch": "3.0.4",
|
||||
"ms": "2.1.3",
|
||||
"open": "8.4.0",
|
||||
"pirates": "4.0.4",
|
||||
"playwright-core": "1.20.0-next",
|
||||
"playwright-core": "1.20.2",
|
||||
"rimraf": "3.0.2",
|
||||
"source-map-support": "0.4.18",
|
||||
"stack-utils": "2.0.5",
|
||||
|
|
@ -11132,12 +11130,9 @@
|
|||
"optional": true
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "4.0.0",
|
||||
|
|
@ -11374,7 +11369,8 @@
|
|||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
|
|
@ -11744,13 +11740,13 @@
|
|||
"playwright": {
|
||||
"version": "file:packages/playwright",
|
||||
"requires": {
|
||||
"playwright-core": "1.20.0-next"
|
||||
"playwright-core": "1.20.2"
|
||||
}
|
||||
},
|
||||
"playwright-chromium": {
|
||||
"version": "file:packages/playwright-chromium",
|
||||
"requires": {
|
||||
"playwright-core": "1.20.0-next"
|
||||
"playwright-core": "1.20.2"
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
|
|
@ -11786,13 +11782,13 @@
|
|||
"playwright-firefox": {
|
||||
"version": "file:packages/playwright-firefox",
|
||||
"requires": {
|
||||
"playwright-core": "1.20.0-next"
|
||||
"playwright-core": "1.20.2"
|
||||
}
|
||||
},
|
||||
"playwright-webkit": {
|
||||
"version": "file:packages/playwright-webkit",
|
||||
"requires": {
|
||||
"playwright-core": "1.20.0-next"
|
||||
"playwright-core": "1.20.2"
|
||||
}
|
||||
},
|
||||
"pngjs": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"private": true,
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-chromium",
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"description": "A high-level API to automate Chromium",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -26,6 +26,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.20.0-next"
|
||||
"playwright-core": "1.20.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-core",
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ const DEFAULT_ARGS = [
|
|||
'--disable-default-apps',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-extensions',
|
||||
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,AcceptCHFrame,AutoExpandDetailsElement',
|
||||
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater',
|
||||
'--allow-pre-commit-input',
|
||||
'--disable-hang-monitor',
|
||||
'--disable-ipc-flooding-protection',
|
||||
|
|
|
|||
|
|
@ -441,7 +441,7 @@ export class Frame extends SdkObject {
|
|||
private _setContentCounter = 0;
|
||||
readonly _detachedPromise: Promise<void>;
|
||||
private _detachedCallback = () => {};
|
||||
private _nonStallingEvaluations = new Set<(error: Error) => void>();
|
||||
private _raceAgainstEvaluationStallingEventsPromises = new Set<ManualPromise<any>>();
|
||||
|
||||
constructor(page: Page, id: string, parentFrame: Frame | null) {
|
||||
super(page, 'frame');
|
||||
|
|
@ -500,53 +500,47 @@ export class Frame extends SdkObject {
|
|||
}
|
||||
|
||||
_invalidateNonStallingEvaluations(message: string) {
|
||||
if (!this._nonStallingEvaluations)
|
||||
if (!this._raceAgainstEvaluationStallingEventsPromises.size)
|
||||
return;
|
||||
const error = new Error(message);
|
||||
for (const callback of this._nonStallingEvaluations)
|
||||
callback(error);
|
||||
for (const promise of this._raceAgainstEvaluationStallingEventsPromises)
|
||||
promise.reject(error);
|
||||
}
|
||||
|
||||
async nonStallingRawEvaluateInExistingMainContext(expression: string): Promise<any> {
|
||||
async raceAgainstEvaluationStallingEvents<T>(cb: () => Promise<T>): Promise<T> {
|
||||
if (this._pendingDocument)
|
||||
throw new Error('Frame is currently attempting a navigation');
|
||||
if (this._page._frameManager._openedDialogs.size)
|
||||
throw new Error('Open JavaScript dialog prevents evaluation');
|
||||
const context = this._existingMainContext();
|
||||
if (!context)
|
||||
throw new Error('Frame does not yet have a main execution context');
|
||||
|
||||
let callback = () => {};
|
||||
const frameInvalidated = new Promise<void>((f, r) => callback = r);
|
||||
this._nonStallingEvaluations.add(callback);
|
||||
const promise = new ManualPromise<T>();
|
||||
this._raceAgainstEvaluationStallingEventsPromises.add(promise);
|
||||
try {
|
||||
return await Promise.race([
|
||||
context.rawEvaluateJSON(expression),
|
||||
frameInvalidated
|
||||
cb(),
|
||||
promise
|
||||
]);
|
||||
} finally {
|
||||
this._nonStallingEvaluations.delete(callback);
|
||||
this._raceAgainstEvaluationStallingEventsPromises.delete(promise);
|
||||
}
|
||||
}
|
||||
|
||||
async nonStallingEvaluateInExistingContext(expression: string, isFunction: boolean|undefined, world: types.World): Promise<any> {
|
||||
if (this._pendingDocument)
|
||||
throw new Error('Frame is currently attempting a navigation');
|
||||
const context = this._contextData.get(world)?.context;
|
||||
if (!context)
|
||||
throw new Error('Frame does not yet have the execution context');
|
||||
nonStallingRawEvaluateInExistingMainContext(expression: string): Promise<any> {
|
||||
return this.raceAgainstEvaluationStallingEvents(() => {
|
||||
const context = this._existingMainContext();
|
||||
if (!context)
|
||||
throw new Error('Frame does not yet have a main execution context');
|
||||
return context.rawEvaluateJSON(expression);
|
||||
});
|
||||
}
|
||||
|
||||
let callback = () => {};
|
||||
const frameInvalidated = new Promise<void>((f, r) => callback = r);
|
||||
this._nonStallingEvaluations.add(callback);
|
||||
try {
|
||||
return await Promise.race([
|
||||
context.evaluateExpression(expression, isFunction),
|
||||
frameInvalidated
|
||||
]);
|
||||
} finally {
|
||||
this._nonStallingEvaluations.delete(callback);
|
||||
}
|
||||
nonStallingEvaluateInExistingContext(expression: string, isFunction: boolean|undefined, world: types.World): Promise<any> {
|
||||
return this.raceAgainstEvaluationStallingEvents(() => {
|
||||
const context = this._contextData.get(world)?.context;
|
||||
if (!context)
|
||||
throw new Error('Frame does not yet have the execution context');
|
||||
return context.evaluateExpression(expression, isFunction);
|
||||
});
|
||||
}
|
||||
|
||||
private _recalculateLifecycle() {
|
||||
|
|
@ -1168,10 +1162,12 @@ export class Frame extends SdkObject {
|
|||
}
|
||||
|
||||
async hideHighlight() {
|
||||
const context = await this._utilityContext();
|
||||
const injectedScript = await context.injectedScript();
|
||||
return await injectedScript.evaluate(injected => {
|
||||
return injected.hideHighlight();
|
||||
return this.raceAgainstEvaluationStallingEvents(async () => {
|
||||
const context = await this._utilityContext();
|
||||
const injectedScript = await context.injectedScript();
|
||||
return await injectedScript.evaluate(injected => {
|
||||
return injected.hideHighlight();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -804,8 +804,11 @@ export class InjectedScript {
|
|||
while (container) {
|
||||
// elementFromPoint works incorrectly in Chromium (http://crbug.com/1188919),
|
||||
// so we use elementsFromPoint instead.
|
||||
const elements = (container as Document).elementsFromPoint(x, y);
|
||||
const innerElement = elements[0] as Element | undefined;
|
||||
const elements: Element[] = container.elementsFromPoint(x, y);
|
||||
let innerElement = elements[0] as Element | undefined;
|
||||
// Workaround https://bugs.chromium.org/p/chromium/issues/detail?id=1307458.
|
||||
if (elements[0] && elements[1] && elements[0].contains(elements[1]) && container.elementFromPoint(x, y) === elements[1])
|
||||
innerElement = elements[1];
|
||||
if (!innerElement || element === innerElement)
|
||||
break;
|
||||
element = innerElement;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import { Progress, ProgressController } from './progress';
|
|||
import { assert, isError } from '../utils/utils';
|
||||
import { ManualPromise } from '../utils/async';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { mimeTypeToComparator, ImageComparatorOptions, ComparatorResult } from '../utils/comparators';
|
||||
import { getComparator, ImageComparatorOptions, ComparatorResult } from '../utils/comparators';
|
||||
import { SelectorInfo, Selectors } from './selectors';
|
||||
import { CallMetadata, SdkObject } from './instrumentation';
|
||||
import { Artifact } from './artifact';
|
||||
|
|
@ -447,7 +447,7 @@ export class Page extends SdkObject {
|
|||
return await this._screenshotter.screenshotPage(progress, options.screenshotOptions || {});
|
||||
};
|
||||
|
||||
const comparator = mimeTypeToComparator['image/png'];
|
||||
const comparator = getComparator('image/png');
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const isGeneratingNewScreenshot = !options.expected;
|
||||
if (isGeneratingNewScreenshot && options.isNot)
|
||||
|
|
|
|||
|
|
@ -222,8 +222,18 @@ export class Screenshotter {
|
|||
}));
|
||||
}
|
||||
|
||||
async _maskElements(progress: Progress, options: ScreenshotOptions) {
|
||||
async _maskElements(progress: Progress, options: ScreenshotOptions): Promise<() => Promise<void>> {
|
||||
const framesToParsedSelectors: MultiMap<Frame, ParsedSelector> = new MultiMap();
|
||||
|
||||
const cleanup = async () => {
|
||||
await Promise.all([...framesToParsedSelectors.keys()].map(async frame => {
|
||||
await frame.hideHighlight();
|
||||
}));
|
||||
};
|
||||
|
||||
if (!options.mask || !options.mask.length)
|
||||
return cleanup;
|
||||
|
||||
await Promise.all((options.mask || []).map(async ({ frame, selector }) => {
|
||||
const pair = await frame.resolveFrameForSelectorNoWait(selector);
|
||||
if (pair)
|
||||
|
|
@ -234,7 +244,8 @@ export class Screenshotter {
|
|||
await Promise.all([...framesToParsedSelectors.keys()].map(async frame => {
|
||||
await frame.maskSelectors(framesToParsedSelectors.get(frame));
|
||||
}));
|
||||
progress.cleanupWhenAborted(() => this._page.hideHighlight());
|
||||
progress.cleanupWhenAborted(cleanup);
|
||||
return cleanup;
|
||||
}
|
||||
|
||||
private async _screenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean | undefined, options: ScreenshotOptions): Promise<Buffer> {
|
||||
|
|
@ -248,13 +259,13 @@ export class Screenshotter {
|
|||
}
|
||||
progress.throwIfAborted(); // Avoid extra work.
|
||||
|
||||
await this._maskElements(progress, options);
|
||||
const cleanupHighlight = await this._maskElements(progress, options);
|
||||
progress.throwIfAborted(); // Avoid extra work.
|
||||
|
||||
const buffer = await this._page._delegate.takeScreenshot(progress, format, documentRect, viewportRect, options.quality, fitsViewport);
|
||||
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
|
||||
|
||||
await this._page.hideHighlight();
|
||||
await cleanupHighlight();
|
||||
progress.throwIfAborted(); // Avoid restoring after failure - should be done by cleanup.
|
||||
|
||||
if (shouldSetDefaultBackground)
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export class Selectors {
|
|||
}
|
||||
|
||||
async _queryCount(frame: frames.Frame, info: SelectorInfo, scope?: dom.ElementHandle): Promise<number> {
|
||||
const context = await frame._utilityContext();
|
||||
const context = await frame._context(info.world);
|
||||
const injectedScript = await context.injectedScript();
|
||||
return await injectedScript.evaluate((injected, { parsed, scope }) => {
|
||||
return injected.querySelectorAll(parsed, scope || document).length;
|
||||
|
|
|
|||
|
|
@ -26,12 +26,16 @@ const { PNG } = require(require.resolve('pngjs', { paths: [require.resolve('pixe
|
|||
export type ImageComparatorOptions = { threshold?: number, maxDiffPixels?: number, maxDiffPixelRatio?: number };
|
||||
export type ComparatorResult = { diff?: Buffer; errorMessage?: string; } | null;
|
||||
export type Comparator = (actualBuffer: Buffer | string, expectedBuffer: Buffer, options?: any) => ComparatorResult;
|
||||
export const mimeTypeToComparator: { [key: string]: Comparator } = {
|
||||
'application/octet-string': compareBuffersOrStrings,
|
||||
'image/png': compareImages.bind(null, 'image/png'),
|
||||
'image/jpeg': compareImages.bind(null, 'image/jpeg'),
|
||||
'text/plain': compareText,
|
||||
};
|
||||
|
||||
export function getComparator(mimeType: string): Comparator {
|
||||
if (mimeType === 'image/png')
|
||||
return compareImages.bind(null, 'image/png');
|
||||
if (mimeType === 'image/jpeg')
|
||||
return compareImages.bind(null, 'image/jpeg');
|
||||
if (mimeType === 'text/plain')
|
||||
return compareText;
|
||||
return compareBuffersOrStrings;
|
||||
}
|
||||
|
||||
function compareBuffersOrStrings(actualBuffer: Buffer | string, expectedBuffer: Buffer): ComparatorResult {
|
||||
if (typeof actualBuffer === 'string')
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ export const deps: any = {
|
|||
'libxi6',
|
||||
'libxrender1',
|
||||
'libxt6',
|
||||
'libxtst6',
|
||||
],
|
||||
webkit: [
|
||||
'gstreamer1.0-libav',
|
||||
|
|
|
|||
38
packages/playwright-core/types/types.d.ts
vendored
38
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -2845,7 +2845,10 @@ export interface Page {
|
|||
}): Promise<null|Response>;
|
||||
|
||||
/**
|
||||
* API testing helper associated with this page. Requests made with this API will use page cookies.
|
||||
* API testing helper associated with this page. This method returns the same instance as
|
||||
* [browserContext.request](https://playwright.dev/docs/api/class-browsercontext#browser-context-request) on the page's
|
||||
* context. See [browserContext.request](https://playwright.dev/docs/api/class-browsercontext#browser-context-request) for
|
||||
* more details.
|
||||
*/
|
||||
request: APIRequestContext;
|
||||
|
||||
|
|
@ -6612,7 +6615,6 @@ export interface BrowserContext {
|
|||
* - `'midi'`
|
||||
* - `'midi-sysex'` (system-exclusive midi)
|
||||
* - `'notifications'`
|
||||
* - `'push'`
|
||||
* - `'camera'`
|
||||
* - `'microphone'`
|
||||
* - `'background-sync'`
|
||||
|
|
@ -11826,9 +11828,10 @@ export interface AndroidWebView {
|
|||
}
|
||||
|
||||
/**
|
||||
* Exposes API that can be used for the Web API testing. Each Playwright browser context has a APIRequestContext instance
|
||||
* attached which shares cookies with the page context. Its also possible to create a new APIRequestContext instance
|
||||
* manually. For more information see [here](https://playwright.dev/docs/class-apirequestcontext).
|
||||
* Exposes API that can be used for the Web API testing. This class is used for creating [APIRequestContext] instance which
|
||||
* in turn can be used for sending web requests. An instance of this class can be obtained via
|
||||
* [playwright.request](https://playwright.dev/docs/api/class-playwright#playwright-request). For more information see
|
||||
* [APIRequestContext].
|
||||
*/
|
||||
export interface APIRequest {
|
||||
/**
|
||||
|
|
@ -11953,9 +11956,28 @@ export interface APIRequest {
|
|||
|
||||
/**
|
||||
* This API is used for the Web API testing. You can use it to trigger API endpoints, configure micro-services, prepare
|
||||
* environment or the service to your e2e test. When used on [Page] or a [BrowserContext], this API will automatically use
|
||||
* the cookies from the corresponding [BrowserContext]. This means that if you log in using this API, your e2e test will be
|
||||
* logged in and vice versa.
|
||||
* environment or the service to your e2e test.
|
||||
*
|
||||
* Each Playwright browser context has associated with it [APIRequestContext] instance which shares cookie storage with the
|
||||
* browser context and can be accessed via
|
||||
* [browserContext.request](https://playwright.dev/docs/api/class-browsercontext#browser-context-request) or
|
||||
* [page.request](https://playwright.dev/docs/api/class-page#page-request). It is also possible to create a new
|
||||
* APIRequestContext instance manually by calling
|
||||
* [apiRequest.newContext([options])](https://playwright.dev/docs/api/class-apirequest#api-request-new-context).
|
||||
*
|
||||
* **Cookie management**
|
||||
*
|
||||
* [APIRequestContext] retuned by
|
||||
* [browserContext.request](https://playwright.dev/docs/api/class-browsercontext#browser-context-request) and
|
||||
* [page.request](https://playwright.dev/docs/api/class-page#page-request) shares cookie storage with the corresponding
|
||||
* [BrowserContext]. Each API request will have `Cookie` header populated with the values from the browser context. If the
|
||||
* API response contains `Set-Cookie` header it will automatically update [BrowserContext] cookies and requests made from
|
||||
* the page will pick them up. This means that if you log in using this API, your e2e test will be logged in and vice
|
||||
* versa.
|
||||
*
|
||||
* If you want API requests to not interfere with the browser cookies you shoud create a new [APIRequestContext] by calling
|
||||
* [apiRequest.newContext([options])](https://playwright.dev/docs/api/class-apirequest#api-request-new-context). Such
|
||||
* `APIRequestContext` object will have its own isolated cookie storage.
|
||||
*
|
||||
*/
|
||||
export interface APIRequestContext {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-firefox",
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"description": "A high-level API to automate Firefox",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -26,6 +26,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.20.0-next"
|
||||
"playwright-core": "1.20.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/test",
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -50,13 +50,13 @@
|
|||
"debug": "4.3.3",
|
||||
"expect": "27.2.5",
|
||||
"jest-matcher-utils": "27.2.5",
|
||||
"json5": "2.2.0",
|
||||
"json5": "2.2.1",
|
||||
"mime": "3.0.0",
|
||||
"minimatch": "3.0.4",
|
||||
"ms": "2.1.3",
|
||||
"open": "8.4.0",
|
||||
"pirates": "4.0.4",
|
||||
"playwright-core": "1.20.0-next",
|
||||
"playwright-core": "1.20.2",
|
||||
"rimraf": "3.0.2",
|
||||
"source-map-support": "0.4.18",
|
||||
"stack-utils": "2.0.5",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
import { Command } from 'commander';
|
||||
import fs from 'fs';
|
||||
import url from 'url';
|
||||
import path from 'path';
|
||||
import type { Config } from './types';
|
||||
import { Runner, builtInReporters, BuiltInReporter, kDefaultConfigFiles } from './runner';
|
||||
|
|
@ -245,7 +246,7 @@ function restartWithExperimentalTsEsm(configFile: string | null): boolean {
|
|||
return false;
|
||||
if (!fileIsModule(configFile))
|
||||
return false;
|
||||
const NODE_OPTIONS = (process.env.NODE_OPTIONS || '') + ` --experimental-loader=${require.resolve('@playwright/test/lib/experimentalLoader')}`;
|
||||
const NODE_OPTIONS = (process.env.NODE_OPTIONS || '') + ` --experimental-loader=${url.pathToFileURL(require.resolve('@playwright/test/lib/experimentalLoader')).toString()}`;
|
||||
const innerProcess = require('child_process').fork(require.resolve('playwright-core/cli'), process.argv.slice(2), {
|
||||
env: {
|
||||
...process.env,
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import {
|
|||
toHaveURL,
|
||||
toHaveValue
|
||||
} from './matchers/matchers';
|
||||
import { toMatchSnapshot, toHaveScreenshot } from './matchers/toMatchSnapshot';
|
||||
import { toMatchSnapshot } from './matchers/toMatchSnapshot';
|
||||
import type { Expect, TestError } from './types';
|
||||
import matchers from 'expect/build/matchers';
|
||||
import { currentTestInfo } from './globals';
|
||||
|
|
@ -132,7 +132,6 @@ const customMatchers = {
|
|||
toHaveURL,
|
||||
toHaveValue,
|
||||
toMatchSnapshot,
|
||||
toHaveScreenshot,
|
||||
};
|
||||
|
||||
type ExpectMetaInfo = {
|
||||
|
|
|
|||
|
|
@ -489,7 +489,7 @@ export function folderIsModule(folder: string): boolean {
|
|||
if (fs.existsSync(packageJson)) {
|
||||
isModule = require(packageJson).type === 'module';
|
||||
} else {
|
||||
const parentFolder = path.basename(folder);
|
||||
const parentFolder = path.dirname(folder);
|
||||
if (parentFolder !== folder)
|
||||
isModule = folderIsModule(parentFolder);
|
||||
else
|
||||
|
|
|
|||
|
|
@ -14,14 +14,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Locator, Page } from 'playwright-core';
|
||||
import type { Page as PageEx } from 'playwright-core/lib/client/page';
|
||||
import type { Locator as LocatorEx } from 'playwright-core/lib/client/locator';
|
||||
import type { Expect } from '../types';
|
||||
import { currentTestInfo } from '../globals';
|
||||
import { mimeTypeToComparator, ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils/comparators';
|
||||
import type { PageScreenshotOptions } from 'playwright-core/types/types';
|
||||
import { addSuffixToFilePath, serializeError, sanitizeForFilePath, trimLongString, callLogText, currentExpectTimeout } from '../util';
|
||||
import { getComparator, ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils/comparators';
|
||||
import { addSuffixToFilePath, serializeError, sanitizeForFilePath, trimLongString, callLogText } from '../util';
|
||||
import { UpdateSnapshots } from '../types';
|
||||
import colors from 'colors/safe';
|
||||
import fs from 'fs';
|
||||
|
|
@ -48,6 +44,7 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
|
|||
readonly kind: 'Screenshot'|'Snapshot';
|
||||
readonly updateSnapshots: UpdateSnapshots;
|
||||
readonly comparatorOptions: ImageComparatorOptions;
|
||||
readonly comparator: Comparator;
|
||||
readonly allOptions: T;
|
||||
|
||||
constructor(
|
||||
|
|
@ -99,10 +96,7 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
|
|||
if (updateSnapshots === 'missing' && testInfo.retry < testInfo.project.retries)
|
||||
updateSnapshots = 'none';
|
||||
const mimeType = mime.getType(path.basename(snapshotPath)) ?? 'application/octet-string';
|
||||
const comparator: Comparator = mimeTypeToComparator[mimeType];
|
||||
if (!comparator)
|
||||
throw new Error('Failed to find comparator with type ' + mimeType + ': ' + snapshotPath);
|
||||
|
||||
this.comparator = getComparator(mimeType);
|
||||
this.testInfo = testInfo;
|
||||
this.mimeType = mimeType;
|
||||
this.actualPath = actualPath;
|
||||
|
|
@ -206,13 +200,11 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
|
|||
}
|
||||
}
|
||||
|
||||
type MatchSnapshotOptions = Omit<ImageComparatorOptions, 'maxDiffPixels' | 'maxDiffPixelRatio'>;
|
||||
|
||||
export function toMatchSnapshot(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
received: Buffer | string,
|
||||
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & MatchSnapshotOptions = {},
|
||||
optOptions: MatchSnapshotOptions = {}
|
||||
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ImageComparatorOptions = {},
|
||||
optOptions: ImageComparatorOptions = {}
|
||||
): SyncExpectationResult {
|
||||
const testInfo = currentTestInfo();
|
||||
if (!testInfo)
|
||||
|
|
@ -221,14 +213,11 @@ export function toMatchSnapshot(
|
|||
testInfo, determineFileExtension(received),
|
||||
testInfo.project.expect?.toMatchSnapshot || {},
|
||||
nameOrOptions, optOptions);
|
||||
const comparator: Comparator = mimeTypeToComparator[helper.mimeType];
|
||||
if (!comparator)
|
||||
throw new Error('Failed to find comparator with type ' + helper.mimeType + ': ' + helper.snapshotPath);
|
||||
|
||||
if (this.isNot) {
|
||||
if (!fs.existsSync(helper.snapshotPath))
|
||||
return helper.handleMissingNegated();
|
||||
const isDifferent = !!comparator(received, fs.readFileSync(helper.snapshotPath), helper.comparatorOptions);
|
||||
const isDifferent = !!helper.comparator(received, fs.readFileSync(helper.snapshotPath), helper.comparatorOptions);
|
||||
return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated();
|
||||
}
|
||||
|
||||
|
|
@ -236,11 +225,7 @@ export function toMatchSnapshot(
|
|||
return helper.handleMissing(received);
|
||||
|
||||
const expected = fs.readFileSync(helper.snapshotPath);
|
||||
const result = comparator(received, expected, {
|
||||
...helper.comparatorOptions,
|
||||
maxDiffPixels: undefined,
|
||||
maxDiffPixelRatio: undefined,
|
||||
});
|
||||
const result = helper.comparator(received, expected, helper.comparatorOptions);
|
||||
if (!result)
|
||||
return helper.handleMatching();
|
||||
|
||||
|
|
@ -254,107 +239,6 @@ export function toMatchSnapshot(
|
|||
return helper.handleDifferent(received, expected, result.diff, result.errorMessage, undefined);
|
||||
}
|
||||
|
||||
type HaveScreenshotOptions = ImageComparatorOptions & Omit<PageScreenshotOptions, 'type' | 'quality' | 'path'>;
|
||||
|
||||
export async function toHaveScreenshot(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
pageOrLocator: Page | Locator,
|
||||
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & HaveScreenshotOptions = {},
|
||||
optOptions: HaveScreenshotOptions = {}
|
||||
): Promise<SyncExpectationResult> {
|
||||
const testInfo = currentTestInfo();
|
||||
if (!testInfo)
|
||||
throw new Error(`toHaveScreenshot() must be called during the test`);
|
||||
const helper = new SnapshotHelper(
|
||||
testInfo, 'png',
|
||||
testInfo.project.expect?.toHaveScreenshot || {},
|
||||
nameOrOptions, optOptions);
|
||||
const [page, locator] = pageOrLocator.constructor.name === 'Page' ? [(pageOrLocator as PageEx), undefined] : [(pageOrLocator as Locator).page() as PageEx, pageOrLocator as LocatorEx];
|
||||
const screenshotOptions = {
|
||||
...helper.allOptions,
|
||||
mask: (helper.allOptions.mask || []) as LocatorEx[],
|
||||
name: undefined,
|
||||
threshold: undefined,
|
||||
maxDiffPixels: undefined,
|
||||
maxDiffPixelRatio: undefined,
|
||||
};
|
||||
|
||||
const hasSnapshot = fs.existsSync(helper.snapshotPath);
|
||||
if (this.isNot) {
|
||||
if (!hasSnapshot)
|
||||
return helper.handleMissingNegated();
|
||||
|
||||
// Having `errorMessage` means we timed out while waiting
|
||||
// for screenshots not to match, so screenshots
|
||||
// are actually the same in the end.
|
||||
const isDifferent = !(await page._expectScreenshot({
|
||||
expected: await fs.promises.readFile(helper.snapshotPath),
|
||||
isNot: true,
|
||||
locator,
|
||||
comparatorOptions: helper.comparatorOptions,
|
||||
screenshotOptions,
|
||||
timeout: currentExpectTimeout(helper.allOptions),
|
||||
})).errorMessage;
|
||||
return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated();
|
||||
}
|
||||
|
||||
// Fast path: there's no screenshot and we don't intend to update it.
|
||||
if (helper.updateSnapshots === 'none' && !hasSnapshot)
|
||||
return { pass: false, message: () => `${helper.snapshotPath} is missing in snapshots.` };
|
||||
|
||||
if (helper.updateSnapshots === 'all' || !hasSnapshot) {
|
||||
// Regenerate a new screenshot by waiting until two screenshots are the same.
|
||||
const timeout = currentExpectTimeout(helper.allOptions);
|
||||
const { actual, previous, diff, errorMessage, log } = await page._expectScreenshot({
|
||||
expected: undefined,
|
||||
isNot: false,
|
||||
locator,
|
||||
comparatorOptions: helper.comparatorOptions,
|
||||
screenshotOptions,
|
||||
timeout,
|
||||
});
|
||||
// We tried re-generating new snapshot but failed.
|
||||
// This can be due to e.g. spinning animation, so we want to show it as a diff.
|
||||
if (errorMessage) {
|
||||
// TODO(aslushnikov): rename attachments to "actual" and "previous". They still should be somehow shown in HTML reporter.
|
||||
const title = actual && previous ?
|
||||
`Timeout ${timeout}ms exceeded while generating screenshot because ${locator ? 'element' : 'page'} kept changing:` :
|
||||
`Timeout ${timeout}ms exceeded while generating screenshot:`;
|
||||
return helper.handleDifferent(actual, previous, diff, undefined, log, title);
|
||||
}
|
||||
|
||||
// We successfully (re-)generated new screenshot.
|
||||
if (!hasSnapshot)
|
||||
return helper.handleMissing(actual!);
|
||||
|
||||
writeFileSync(helper.snapshotPath, actual!);
|
||||
/* eslint-disable no-console */
|
||||
console.log(helper.snapshotPath + ' is re-generated, writing actual.');
|
||||
return {
|
||||
pass: true,
|
||||
message: () => helper.snapshotPath + ' running with --update-snapshots, writing actual.'
|
||||
};
|
||||
}
|
||||
|
||||
// General case:
|
||||
// - snapshot exists
|
||||
// - regular matcher (i.e. not a `.not`)
|
||||
// - no flags to update screenshots
|
||||
const expected = await fs.promises.readFile(helper.snapshotPath);
|
||||
const { actual, diff, errorMessage, log } = await page._expectScreenshot({
|
||||
expected,
|
||||
isNot: false,
|
||||
locator,
|
||||
comparatorOptions: helper.comparatorOptions,
|
||||
screenshotOptions,
|
||||
timeout: currentExpectTimeout(helper.allOptions),
|
||||
});
|
||||
|
||||
return errorMessage ?
|
||||
helper.handleDifferent(actual, expected, diff, errorMessage, log) :
|
||||
helper.handleMatching();
|
||||
}
|
||||
|
||||
function writeFileSync(aPath: string, content: Buffer | string) {
|
||||
fs.mkdirSync(path.dirname(aPath), { recursive: true });
|
||||
fs.writeFileSync(aPath, content);
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ export class Runner {
|
|||
fatalErrors.push(createNoTestsError());
|
||||
|
||||
// 8. Compute shards.
|
||||
let testGroups = createTestGroups(rootSuite);
|
||||
let testGroups = createTestGroups(rootSuite, config.workers);
|
||||
|
||||
const shard = config.shard;
|
||||
if (shard) {
|
||||
|
|
@ -619,7 +619,7 @@ function buildItemLocation(rootDir: string, testOrSuite: Suite | TestCase) {
|
|||
return `${path.relative(rootDir, testOrSuite.location.file)}:${testOrSuite.location.line}`;
|
||||
}
|
||||
|
||||
function createTestGroups(rootSuite: Suite): TestGroup[] {
|
||||
function createTestGroups(rootSuite: Suite, workers: number): TestGroup[] {
|
||||
// This function groups tests that can be run together.
|
||||
// Tests cannot be run together when:
|
||||
// - They belong to different projects - requires different workers.
|
||||
|
|
@ -630,7 +630,15 @@ function createTestGroups(rootSuite: Suite): TestGroup[] {
|
|||
|
||||
// Using the map "workerHash -> requireFile -> group" makes us preserve the natural order
|
||||
// of worker hashes and require files for the simple cases.
|
||||
const groups = new Map<string, Map<string, { general: TestGroup, parallel: TestGroup[] }>>();
|
||||
const groups = new Map<string, Map<string, {
|
||||
// Tests that must be run in order are in the same group.
|
||||
general: TestGroup,
|
||||
// Tests that may be run independently each has a dedicated group with a single test.
|
||||
parallel: TestGroup[],
|
||||
// Tests that are marked as parallel but have beforeAll/afterAll hooks should be grouped
|
||||
// as much as possible. We split them into equally sized groups, one per worker.
|
||||
parallelWithHooks: TestGroup,
|
||||
}>>();
|
||||
|
||||
const createGroup = (test: TestCase): TestGroup => {
|
||||
return {
|
||||
|
|
@ -654,18 +662,26 @@ function createTestGroups(rootSuite: Suite): TestGroup[] {
|
|||
withRequireFile = {
|
||||
general: createGroup(test),
|
||||
parallel: [],
|
||||
parallelWithHooks: createGroup(test),
|
||||
};
|
||||
withWorkerHash.set(test._requireFile, withRequireFile);
|
||||
}
|
||||
|
||||
let insideParallel = false;
|
||||
for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent)
|
||||
let hasAllHooks = false;
|
||||
for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent) {
|
||||
insideParallel = insideParallel || parent._parallelMode === 'parallel';
|
||||
hasAllHooks = hasAllHooks || parent.hooks.length > 0;
|
||||
}
|
||||
|
||||
if (insideParallel) {
|
||||
const group = createGroup(test);
|
||||
group.tests.push(test);
|
||||
withRequireFile.parallel.push(group);
|
||||
if (hasAllHooks) {
|
||||
withRequireFile.parallelWithHooks.tests.push(test);
|
||||
} else {
|
||||
const group = createGroup(test);
|
||||
group.tests.push(test);
|
||||
withRequireFile.parallel.push(group);
|
||||
}
|
||||
} else {
|
||||
withRequireFile.general.tests.push(test);
|
||||
}
|
||||
|
|
@ -678,6 +694,16 @@ function createTestGroups(rootSuite: Suite): TestGroup[] {
|
|||
if (withRequireFile.general.tests.length)
|
||||
result.push(withRequireFile.general);
|
||||
result.push(...withRequireFile.parallel);
|
||||
|
||||
const parallelWithHooksGroupSize = Math.ceil(withRequireFile.parallelWithHooks.tests.length / workers);
|
||||
let lastGroup: TestGroup | undefined;
|
||||
for (const test of withRequireFile.parallelWithHooks.tests) {
|
||||
if (!lastGroup || lastGroup.tests.length >= parallelWithHooksGroupSize) {
|
||||
lastGroup = createGroup(test);
|
||||
result.push(lastGroup);
|
||||
}
|
||||
lastGroup.tests.push(test);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -99,25 +99,33 @@ export function resolveHook(filename: string, specifier: string): string | undef
|
|||
if (!isTypeScript)
|
||||
return;
|
||||
const tsconfig = loadAndValidateTsconfigForFile(filename);
|
||||
if (!tsconfig)
|
||||
return;
|
||||
for (const { key, values } of tsconfig.paths) {
|
||||
const keyHasStar = key[key.length - 1] === '*';
|
||||
const matches = specifier.startsWith(keyHasStar ? key.substring(0, key.length - 1) : key);
|
||||
if (!matches)
|
||||
continue;
|
||||
for (const value of values) {
|
||||
const valueHasStar = value[value.length - 1] === '*';
|
||||
let candidate = valueHasStar ? value.substring(0, value.length - 1) : value;
|
||||
if (valueHasStar && keyHasStar)
|
||||
candidate += specifier.substring(key.length - 1);
|
||||
candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate.replace(/\//g, path.sep));
|
||||
for (const ext of ['', '.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx']) {
|
||||
if (fs.existsSync(candidate + ext))
|
||||
return candidate;
|
||||
if (tsconfig) {
|
||||
for (const { key, values } of tsconfig.paths) {
|
||||
const keyHasStar = key[key.length - 1] === '*';
|
||||
const matches = specifier.startsWith(keyHasStar ? key.substring(0, key.length - 1) : key);
|
||||
if (!matches)
|
||||
continue;
|
||||
for (const value of values) {
|
||||
const valueHasStar = value[value.length - 1] === '*';
|
||||
let candidate = valueHasStar ? value.substring(0, value.length - 1) : value;
|
||||
if (valueHasStar && keyHasStar)
|
||||
candidate += specifier.substring(key.length - 1);
|
||||
candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate.replace(/\//g, path.sep));
|
||||
for (const ext of ['', '.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx']) {
|
||||
if (fs.existsSync(candidate + ext))
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (specifier.endsWith('.js')) {
|
||||
const resolved = path.resolve(path.dirname(filename), specifier);
|
||||
if (resolved.endsWith('.js')) {
|
||||
const tsResolved = resolved.substring(0, resolved.length - 3) + '.ts';
|
||||
if (!fs.existsSync(resolved) && fs.existsSync(tsResolved))
|
||||
return tsResolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function transformHook(code: string, filename: string, isModule = false): string {
|
||||
|
|
|
|||
19
packages/playwright-test/types/test.d.ts
vendored
19
packages/playwright-test/types/test.d.ts
vendored
|
|
@ -43,7 +43,7 @@ type ExpectSettings = {
|
|||
* Default timeout for async expect matchers in milliseconds, defaults to 5000ms.
|
||||
*/
|
||||
timeout?: number;
|
||||
toHaveScreenshot?: {
|
||||
toMatchSnapshot?: {
|
||||
/** An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between pixels in compared images, between zero (strict) and one (lax). Defaults to `0.2`.
|
||||
*/
|
||||
threshold?: number,
|
||||
|
|
@ -56,11 +56,6 @@ type ExpectSettings = {
|
|||
*/
|
||||
maxDiffPixelRatio?: number,
|
||||
}
|
||||
toMatchSnapshot?: {
|
||||
/** An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between pixels in compared images, between zero (strict) and one (lax). Defaults to `0.2`.
|
||||
*/
|
||||
threshold?: number,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -162,8 +157,7 @@ interface TestProject {
|
|||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* The base directory, relative to the config file, for snapshot files created with `toMatchSnapshot` and
|
||||
* `toHaveScreenshot`. Defaults to
|
||||
* The base directory, relative to the config file, for snapshot files created with `toMatchSnapshot`. Defaults to
|
||||
* [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).
|
||||
*
|
||||
* The directory for each test can be accessed by
|
||||
|
|
@ -701,7 +695,7 @@ interface TestConfig {
|
|||
* const config: PlaywrightTestConfig = {
|
||||
* expect: {
|
||||
* timeout: 10000,
|
||||
* toHaveScreenshot: {
|
||||
* toMatchSnapshot: {
|
||||
* maxDiffPixels: 10,
|
||||
* },
|
||||
* },
|
||||
|
|
@ -717,8 +711,7 @@ interface TestConfig {
|
|||
metadata?: any;
|
||||
name?: string;
|
||||
/**
|
||||
* The base directory, relative to the config file, for snapshot files created with `toMatchSnapshot` and
|
||||
* `toHaveScreenshot`. Defaults to
|
||||
* The base directory, relative to the config file, for snapshot files created with `toMatchSnapshot`. Defaults to
|
||||
* [testConfig.testDir](https://playwright.dev/docs/api/class-testconfig#test-config-test-dir).
|
||||
*
|
||||
* The directory for each test can be accessed by
|
||||
|
|
@ -1572,8 +1565,8 @@ export interface TestInfo {
|
|||
/**
|
||||
* Suffix used to differentiate snapshots between multiple test configurations. For example, if snapshots depend on the
|
||||
* platform, you can set `testInfo.snapshotSuffix` equal to `process.platform`. In this case both
|
||||
* `expect(value).toMatchSnapshot(snapshotName)` and `expect(page).toHaveScreenshot(snapshotName)` will use different
|
||||
* snapshots depending on the platform. Learn more about [snapshots](https://playwright.dev/docs/test-snapshots).
|
||||
* `expect(value).toMatchSnapshot(snapshotName)` will use different snapshots depending on the platform. Learn more about
|
||||
* [snapshots](https://playwright.dev/docs/test-snapshots).
|
||||
*/
|
||||
snapshotSuffix: string;
|
||||
/**
|
||||
|
|
|
|||
28
packages/playwright-test/types/testExpect.d.ts
vendored
28
packages/playwright-test/types/testExpect.d.ts
vendored
|
|
@ -34,6 +34,8 @@ export declare type Expect = {
|
|||
extend(arg0: any): void;
|
||||
getState(): expect.MatcherState;
|
||||
setState(state: Partial<expect.MatcherState>): void;
|
||||
any(expectedObject: any): AsymmetricMatcher;
|
||||
anything(): AsymmetricMatcher;
|
||||
arrayContaining(sample: Array<unknown>): AsymmetricMatcher;
|
||||
objectContaining(sample: Record<string, unknown>): AsymmetricMatcher;
|
||||
stringContaining(expected: string): AsymmetricMatcher;
|
||||
|
|
@ -43,8 +45,6 @@ export declare type Expect = {
|
|||
* - assertions()
|
||||
* - extractExpectedAssertionsErrors()
|
||||
* – hasAssertions()
|
||||
* - any()
|
||||
* - anything()
|
||||
*/
|
||||
};
|
||||
|
||||
|
|
@ -222,18 +222,6 @@ interface LocatorMatchers {
|
|||
* Asserts given DOM node visible on the screen.
|
||||
*/
|
||||
toBeVisible(options?: { timeout?: number }): Promise<Locator>;
|
||||
|
||||
/**
|
||||
* Asserts element's screenshot is matching to the snapshot.
|
||||
*/
|
||||
toHaveScreenshot(options?: Omit<LocatorScreenshotOptions, 'path' | 'type' | 'quality'> & ImageComparatorOptions & {
|
||||
name?: string | string[],
|
||||
}): Promise<Locator>;
|
||||
|
||||
/**
|
||||
* Asserts element's screenshot is matching to the snapshot.
|
||||
*/
|
||||
toHaveScreenshot(name: string | string[], options?: Omit<LocatorScreenshotOptions, 'path' | 'type' | 'quality'> & ImageComparatorOptions): Promise<Locator>;
|
||||
}
|
||||
interface PageMatchers {
|
||||
/**
|
||||
|
|
@ -245,18 +233,6 @@ interface PageMatchers {
|
|||
* Asserts page's URL.
|
||||
*/
|
||||
toHaveURL(expected: string | RegExp, options?: { timeout?: number }): Promise<Page>;
|
||||
|
||||
/**
|
||||
* Asserts page screenshot is matching to the snapshot.
|
||||
*/
|
||||
toHaveScreenshot(options?: Omit<PageScreenshotOptions, 'path' | 'quality' | 'type'> & ImageComparatorOptions & {
|
||||
name?: string | string[],
|
||||
}): Promise<Page>;
|
||||
|
||||
/**
|
||||
* Asserts page screenshot is matching to the snapshot.
|
||||
*/
|
||||
toHaveScreenshot(name: string | string[], options?: Omit<PageScreenshotOptions, 'path' | 'quality' | 'type'> & ImageComparatorOptions): Promise<Page>;
|
||||
}
|
||||
|
||||
interface APIResponseMatchers {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-webkit",
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"description": "A high-level API to automate WebKit",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -25,6 +25,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.20.0-next"
|
||||
"playwright-core": "1.20.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright",
|
||||
"version": "1.20.0-next",
|
||||
"version": "1.20.2",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -26,6 +26,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.20.0-next"
|
||||
"playwright-core": "1.20.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -184,3 +184,62 @@ it('should work with drag and drop that moves the element under cursor', async (
|
|||
await page.dragAndDrop('#from', '#to');
|
||||
await expect(page.locator('#to')).toHaveText('Dropped');
|
||||
});
|
||||
|
||||
it('should work with block inside inline', async ({ page, server }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`
|
||||
<div>
|
||||
<span>
|
||||
<div id="target" onclick="window._clicked=true">
|
||||
Romimine
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
`);
|
||||
await page.locator('#target').click();
|
||||
expect(await page.evaluate('window._clicked')).toBe(true);
|
||||
});
|
||||
|
||||
it('should work with block-block-block inside inline-inline', async ({ page, server }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`
|
||||
<div>
|
||||
<a href="#ney">
|
||||
<div>
|
||||
<span>
|
||||
<a href="#yay">
|
||||
<div>
|
||||
<h3 id="target">
|
||||
Romimine
|
||||
</h3>
|
||||
</div>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`);
|
||||
await page.locator('#target').click();
|
||||
await expect(page).toHaveURL(server.EMPTY_PAGE + '#yay');
|
||||
});
|
||||
|
||||
it('should work with block inside inline in shadow dom', async ({ page, server }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`
|
||||
<div>
|
||||
</div>
|
||||
<script>
|
||||
const root = document.querySelector('div');
|
||||
const shadowRoot = root.attachShadow({ mode: 'open' });
|
||||
const span = document.createElement('span');
|
||||
shadowRoot.appendChild(span);
|
||||
const div = document.createElement('div');
|
||||
span.appendChild(div);
|
||||
div.id = 'target';
|
||||
div.addEventListener('click', () => window._clicked = true);
|
||||
div.textContent = 'Hello';
|
||||
</script>
|
||||
`);
|
||||
await page.locator('#target').click();
|
||||
expect(await page.evaluate('window._clicked')).toBe(true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
import { test as it, expect } from './pageTest';
|
||||
import { verifyViewport, attachFrame } from '../config/utils';
|
||||
import type { Route } from 'playwright-core';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
|
|
@ -429,6 +430,33 @@ it.describe('page screenshot', () => {
|
|||
const screenshot2 = await page.screenshot();
|
||||
expect(screenshot1.equals(screenshot2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should work when subframe has stalled navigation', async ({ page, server }) => {
|
||||
let cb;
|
||||
const routeReady = new Promise<Route>(f => cb = f);
|
||||
await page.route('**/subframe.html', cb); // Stalling subframe.
|
||||
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const done = page.setContent(`<iframe src='/subframe.html'></iframe>`);
|
||||
const route = await routeReady;
|
||||
|
||||
await page.screenshot({ mask: [ page.locator('non-existent') ] });
|
||||
await route.fulfill({ body: '' });
|
||||
await done;
|
||||
});
|
||||
|
||||
it('should work when subframe used document.open after a weird url', async ({ page, server }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.evaluate(() => {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = 'javascript:hi';
|
||||
document.body.appendChild(iframe);
|
||||
iframe.contentDocument.open();
|
||||
iframe.contentDocument.write('Hello');
|
||||
iframe.contentDocument.close();
|
||||
});
|
||||
await page.screenshot({ mask: [ page.locator('non-existent') ] });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -30,10 +30,14 @@ for (const [name, url] of Object.entries(vues)) {
|
|||
|
||||
it('should work with single-root elements #smoke', async ({ page }) => {
|
||||
expect(await page.$$eval(`_vue=book-list`, els => els.length)).toBe(1);
|
||||
expect(await page.locator(`_vue=book-list`).count()).toBe(1);
|
||||
await expect(page.locator(`_vue=book-list`)).toHaveCount(1);
|
||||
expect(await page.$$eval(`_vue=book-item`, els => els.length)).toBe(3);
|
||||
expect(await page.locator(`_vue=book-item`).count()).toBe(3);
|
||||
await expect(page.locator(`_vue=book-item`)).toHaveCount(3);
|
||||
expect(await page.$$eval(`_vue=book-list >> _vue=book-item`, els => els.length)).toBe(3);
|
||||
expect(await page.locator(`_vue=book-list >> _vue=book-item`).count()).toBe(3);
|
||||
expect(await page.$$eval(`_vue=book-item >> _vue=book-list`, els => els.length)).toBe(0);
|
||||
|
||||
});
|
||||
|
||||
it('should work with multi-root elements (fragments)', async ({ page }) => {
|
||||
|
|
|
|||
126
tests/playwright-test/esm.spec.ts
Normal file
126
tests/playwright-test/esm.spec.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { test, expect } from './playwright-test-fixtures';
|
||||
|
||||
// Note: tests from this file are additionally run on Node16 bots.
|
||||
|
||||
test('should load nested as esm when package.json has type module', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.js': `
|
||||
//@no-header
|
||||
import * as fs from 'fs';
|
||||
export default { projects: [{name: 'foo'}] };
|
||||
`,
|
||||
'package.json': JSON.stringify({ type: 'module' }),
|
||||
'nested/folder/a.esm.test.js': `
|
||||
const { test } = pwt;
|
||||
test('check project name', ({}, testInfo) => {
|
||||
expect(testInfo.project.name).toBe('foo');
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
});
|
||||
|
||||
test('should import esm from ts when package.json has type module in experimental mode', async ({ runInlineTest }) => {
|
||||
// We only support experimental esm mode on Node 16+
|
||||
test.skip(parseInt(process.version.slice(1), 10) < 16);
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
import * as fs from 'fs';
|
||||
export default { projects: [{name: 'foo'}] };
|
||||
`,
|
||||
'package.json': JSON.stringify({ type: 'module' }),
|
||||
'a.test.ts': `
|
||||
import { foo } from './b.ts';
|
||||
import { bar } from './c.js';
|
||||
import { qux } from './d.js';
|
||||
const { test } = pwt;
|
||||
test('check project name', ({}, testInfo) => {
|
||||
expect(testInfo.project.name).toBe('foo');
|
||||
expect(bar).toBe('bar');
|
||||
expect(qux).toBe('qux');
|
||||
});
|
||||
`,
|
||||
'b.ts': `
|
||||
export const foo: string = 'foo';
|
||||
`,
|
||||
'c.ts': `
|
||||
export const bar: string = 'bar';
|
||||
`,
|
||||
'd.js': `
|
||||
//@no-header
|
||||
export const qux = 'qux';
|
||||
`,
|
||||
}, {}, { PW_EXPERIMENTAL_TS_ESM: true });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should propagate subprocess exit code in experimental mode', async ({ runInlineTest }) => {
|
||||
// We only support experimental esm mode on Node 16+
|
||||
test.skip(parseInt(process.version.slice(1), 10) < 16);
|
||||
const result = await runInlineTest({
|
||||
'package.json': JSON.stringify({ type: 'module' }),
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('failing test', ({}, testInfo) => {
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
`,
|
||||
}, {}, { PW_EXPERIMENTAL_TS_ESM: true });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should respect path resolver in experimental mode', async ({ runInlineTest }) => {
|
||||
// We only support experimental esm mode on Node 16+
|
||||
test.skip(parseInt(process.version.slice(1), 10) < 16);
|
||||
const result = await runInlineTest({
|
||||
'package.json': JSON.stringify({ type: 'module' }),
|
||||
'playwright.config.ts': `
|
||||
export default {
|
||||
projects: [{name: 'foo'}],
|
||||
};
|
||||
`,
|
||||
'tsconfig.json': `{
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"module": "commonjs",
|
||||
"lib": ["esnext", "dom", "DOM.Iterable"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"util/*": ["./foo/bar/util/*"],
|
||||
},
|
||||
},
|
||||
}`,
|
||||
'a.test.ts': `
|
||||
import { foo } from 'util/b.ts';
|
||||
const { test } = pwt;
|
||||
test('check project name', ({}, testInfo) => {
|
||||
expect(testInfo.project.name).toBe(foo);
|
||||
});
|
||||
`,
|
||||
'foo/bar/util/b.ts': `
|
||||
export const foo: string = 'foo';
|
||||
`,
|
||||
}, {}, { PW_EXPERIMENTAL_TS_ESM: true });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
|
@ -118,17 +118,31 @@ test('should include custom error message with web-first assertions', async ({ r
|
|||
].join('\n'));
|
||||
});
|
||||
|
||||
test('should work with default expect prototype functions', async ({ runTSC }) => {
|
||||
const result = await runTSC({
|
||||
'a.spec.ts': `
|
||||
const { test } = pwt;
|
||||
test('should work with default expect prototype functions', async ({ runTSC, runInlineTest }) => {
|
||||
const spec = `
|
||||
const { test } = pwt;
|
||||
test('pass', async () => {
|
||||
const expected = [1, 2, 3, 4, 5, 6];
|
||||
test.expect([4, 1, 6, 7, 3, 5, 2, 5, 4, 6]).toEqual(
|
||||
expect.arrayContaining(expected),
|
||||
);
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect('foo').toEqual(expect.any(String));
|
||||
expect('foo').toEqual(expect.anything());
|
||||
});
|
||||
`;
|
||||
{
|
||||
const result = await runTSC({
|
||||
'a.spec.ts': spec,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
}
|
||||
{
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': spec,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
test('should work with default expect matchers', async ({ runTSC }) => {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import colors from 'colors/safe';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect, stripAnsi } from './playwright-test-fixtures';
|
||||
import { test, expect, stripAnsi, createWhiteImage, paintBlackPixels } from './playwright-test-fixtures';
|
||||
|
||||
const files = {
|
||||
'helper.ts': `
|
||||
|
|
@ -44,6 +44,22 @@ test('should support golden', async ({ runInlineTest }) => {
|
|||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should work with non-txt extensions', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.csv': `1,2,3`,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect('1,2,4').toMatchSnapshot('snapshot.csv');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(stripAnsi(result.output)).toContain(`1,2,34`);
|
||||
});
|
||||
|
||||
|
||||
test('should generate default name', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
|
|
@ -79,6 +95,8 @@ test('should compile with different option combinations', async ({ runTSC }) =>
|
|||
test('is a test', async ({ page }) => {
|
||||
expect('foo').toMatchSnapshot();
|
||||
expect('foo').toMatchSnapshot({ threshold: 0.2 });
|
||||
expect('foo').toMatchSnapshot({ maxDiffPixelRatio: 0.2 });
|
||||
expect('foo').toMatchSnapshot({ maxDiffPixels: 0.2 });
|
||||
});
|
||||
`
|
||||
});
|
||||
|
|
@ -394,6 +412,106 @@ test('should compare binary', async ({ runInlineTest }) => {
|
|||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should respect maxDiffPixels option', async ({ runInlineTest }) => {
|
||||
const width = 20, height = 20;
|
||||
const BAD_PIXELS = 120;
|
||||
const image1 = createWhiteImage(width, height);
|
||||
const image2 = paintBlackPixels(image1, BAD_PIXELS);
|
||||
|
||||
await test.step('make sure default comparison fails', async () => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': image1,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(stripAnsi(result.output)).toContain('120 pixels');
|
||||
expect(stripAnsi(result.output)).toContain('ratio 0.30');
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': image1,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', {
|
||||
maxDiffPixels: ${BAD_PIXELS}
|
||||
});
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure maxDiffPixels option is respected').toBe(0);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'playwright.config.ts': `
|
||||
module.exports = { projects: [
|
||||
{ expect: { toMatchSnapshot: { maxDiffPixels: ${BAD_PIXELS} } } },
|
||||
]};
|
||||
`,
|
||||
'a.spec.js-snapshots/snapshot.png': image1,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure maxDiffPixels option in project config is respected').toBe(0);
|
||||
});
|
||||
|
||||
test('should respect maxDiffPixelRatio option', async ({ runInlineTest }) => {
|
||||
const width = 20, height = 20;
|
||||
const BAD_RATIO = 0.25;
|
||||
const BAD_PIXELS = Math.floor(width * height * BAD_RATIO);
|
||||
const image1 = createWhiteImage(width, height);
|
||||
const image2 = paintBlackPixels(image1, BAD_PIXELS);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': image1,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure default comparison fails').toBe(1);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': image1,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png', {
|
||||
maxDiffPixelRatio: ${BAD_RATIO}
|
||||
});
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure maxDiffPixelRatio option is respected').toBe(0);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'playwright.config.ts': `
|
||||
module.exports = { projects: [
|
||||
{ expect: { toMatchSnapshot: { maxDiffPixelRatio: ${BAD_RATIO} } } },
|
||||
]};
|
||||
`,
|
||||
'a.spec.js-snapshots/snapshot.png': image1,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('${image2.toString('base64')}', 'base64')).toMatchSnapshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure maxDiffPixels option in project config is respected').toBe(0);
|
||||
});
|
||||
|
||||
test('should compare PNG images', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
|
|
@ -417,11 +535,7 @@ test('should compare different PNG images', async ({ runInlineTest }, testInfo)
|
|||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', ({}) => {
|
||||
expect(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII==', 'base64')).toMatchSnapshot('snapshot.png', {
|
||||
// make sure maxDiffPixelRatio is *not* respected.
|
||||
// See https://github.com/microsoft/playwright/issues/12564
|
||||
maxDiffPixelRatio: 1.0,
|
||||
});
|
||||
expect(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII==', 'base64')).toMatchSnapshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
});
|
||||
|
|
|
|||
|
|
@ -241,50 +241,6 @@ test('should fail to load ts from esm when package.json has type module', async
|
|||
expect(result.output).toContain('Cannot import a typescript file from an esmodule');
|
||||
});
|
||||
|
||||
test('should import esm from ts when package.json has type module in experimental mode', async ({ runInlineTest }) => {
|
||||
// We only support experimental esm mode on Node 16+
|
||||
test.skip(parseInt(process.version.slice(1), 10) < 16);
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
import * as fs from 'fs';
|
||||
export default { projects: [{name: 'foo'}] };
|
||||
`,
|
||||
'package.json': JSON.stringify({ type: 'module' }),
|
||||
'a.test.ts': `
|
||||
import { foo } from './b.ts';
|
||||
const { test } = pwt;
|
||||
test('check project name', ({}, testInfo) => {
|
||||
expect(testInfo.project.name).toBe('foo');
|
||||
});
|
||||
`,
|
||||
'b.ts': `
|
||||
export const foo: string = 'foo';
|
||||
`
|
||||
}, {}, {
|
||||
PW_EXPERIMENTAL_TS_ESM: true
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should propagate subprocess exit code in experimental mode', async ({ runInlineTest }) => {
|
||||
// We only support experimental esm mode on Node 16+
|
||||
test.skip(parseInt(process.version.slice(1), 10) < 16);
|
||||
const result = await runInlineTest({
|
||||
'package.json': JSON.stringify({ type: 'module' }),
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('failing test', ({}, testInfo) => {
|
||||
expect(1).toBe(2);
|
||||
});
|
||||
`,
|
||||
}, {}, {
|
||||
PW_EXPERIMENTAL_TS_ESM: true
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should filter stack trace for simple expect', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'expect-test.spec.ts': `
|
||||
|
|
|
|||
|
|
@ -241,7 +241,8 @@ const TSCONFIG = {
|
|||
'esModuleInterop': true,
|
||||
'allowSyntheticDefaultImports': true,
|
||||
'rootDir': '.',
|
||||
'lib': ['esnext', 'dom', 'DOM.Iterable']
|
||||
'lib': ['esnext', 'dom', 'DOM.Iterable'],
|
||||
'noEmit': true,
|
||||
},
|
||||
'exclude': [
|
||||
'node_modules'
|
||||
|
|
|
|||
|
|
@ -163,41 +163,3 @@ test('should respect baseurl w/o paths', async ({ runInlineTest }) => {
|
|||
expect(result.passed).toBe(1);
|
||||
expect(result.output).not.toContain(`Could not`);
|
||||
});
|
||||
|
||||
test('should respect path resolver in experimental mode', async ({ runInlineTest }) => {
|
||||
// We only support experimental esm mode on Node 16+
|
||||
test.skip(parseInt(process.version.slice(1), 10) < 16);
|
||||
const result = await runInlineTest({
|
||||
'package.json': JSON.stringify({ type: 'module' }),
|
||||
'playwright.config.ts': `
|
||||
export default {
|
||||
projects: [{name: 'foo'}],
|
||||
};
|
||||
`,
|
||||
'tsconfig.json': `{
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"module": "commonjs",
|
||||
"lib": ["esnext", "dom", "DOM.Iterable"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"util/*": ["./foo/bar/util/*"],
|
||||
},
|
||||
},
|
||||
}`,
|
||||
'a.test.ts': `
|
||||
import { foo } from 'util/b.ts';
|
||||
const { test } = pwt;
|
||||
test('check project name', ({}, testInfo) => {
|
||||
expect(testInfo.project.name).toBe(foo);
|
||||
});
|
||||
`,
|
||||
'foo/bar/util/b.ts': `
|
||||
export const foo: string = 'foo';
|
||||
`,
|
||||
}, {}, {
|
||||
PW_EXPERIMENTAL_TS_ESM: true
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { test, expect } from './playwright-test-fixtures';
|
||||
import { test, expect, countTimes, stripAnsi } from './playwright-test-fixtures';
|
||||
|
||||
test('test.describe.parallel should throw inside test.describe.serial', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
|
|
@ -176,3 +176,49 @@ test('project.fullyParallel should work', async ({ runInlineTest }) => {
|
|||
expect(result.output).toContain('%% worker=1');
|
||||
expect(result.output).toContain('%% worker=2');
|
||||
});
|
||||
|
||||
test('parallel mode should minimize running beforeAll/afterAll hooks', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
test.beforeAll(() => {
|
||||
console.log('\\n%%beforeAll');
|
||||
});
|
||||
test.afterAll(() => {
|
||||
console.log('\\n%%afterAll');
|
||||
});
|
||||
test('test1', () => {});
|
||||
test('test2', () => {});
|
||||
test('test3', () => {});
|
||||
test('test4', () => {});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(4);
|
||||
expect(countTimes(stripAnsi(result.output), '%%beforeAll')).toBe(1);
|
||||
expect(countTimes(stripAnsi(result.output), '%%afterAll')).toBe(1);
|
||||
});
|
||||
|
||||
test('parallel mode should minimize running beforeAll/afterAll hooks 2', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
test.beforeAll(() => {
|
||||
console.log('\\n%%beforeAll');
|
||||
});
|
||||
test.afterAll(() => {
|
||||
console.log('\\n%%afterAll');
|
||||
});
|
||||
test('test1', () => {});
|
||||
test('test2', () => {});
|
||||
test('test3', () => {});
|
||||
test('test4', () => {});
|
||||
`,
|
||||
}, { workers: 2 });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(4);
|
||||
expect(countTimes(stripAnsi(result.output), '%%beforeAll')).toBe(2);
|
||||
expect(countTimes(stripAnsi(result.output), '%%afterAll')).toBe(2);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,737 +0,0 @@
|
|||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { mimeTypeToComparator } from 'playwright-core/lib/utils/comparators';
|
||||
import * as fs from 'fs';
|
||||
import { PNG } from 'pngjs';
|
||||
import * as path from 'path';
|
||||
import { pathToFileURL } from 'url';
|
||||
import { test, expect, stripAnsi, createImage, paintBlackPixels } from './playwright-test-fixtures';
|
||||
|
||||
const pngComparator = mimeTypeToComparator['image/png'];
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
const IMG_WIDTH = 1280;
|
||||
const IMG_HEIGHT = 720;
|
||||
const whiteImage = createImage(IMG_WIDTH, IMG_HEIGHT, 255, 255, 255);
|
||||
const redImage = createImage(IMG_WIDTH, IMG_HEIGHT, 255, 0, 0);
|
||||
const greenImage = createImage(IMG_WIDTH, IMG_HEIGHT, 0, 255, 0);
|
||||
const blueImage = createImage(IMG_WIDTH, IMG_HEIGHT, 0, 0, 255);
|
||||
|
||||
const files = {
|
||||
'helper.ts': `
|
||||
export const test = pwt.test.extend({
|
||||
auto: [ async ({}, run, testInfo) => {
|
||||
testInfo.snapshotSuffix = '';
|
||||
await run();
|
||||
}, { auto: true } ]
|
||||
});
|
||||
`
|
||||
};
|
||||
|
||||
test('should fail to screenshot a page with infinite animation', async ({ runInlineTest }, testInfo) => {
|
||||
const infiniteAnimationURL = pathToFileURL(path.join(__dirname, '../assets/rotate-z.html'));
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await page.goto('${infiniteAnimationURL}');
|
||||
await expect(page).toHaveScreenshot({ timeout: 2000 });
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(stripAnsi(result.output)).toContain(`Timeout 2000ms exceeded while generating screenshot because page kept changing`);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.png'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-expected.png'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-diff.png'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-1.png'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should not fail when racing with navigation', async ({ runInlineTest }, testInfo) => {
|
||||
const infiniteAnimationURL = pathToFileURL(path.join(__dirname, '../assets/rotate-z.html'));
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': createImage(10, 10, 255, 0, 0),
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.goto('${infiniteAnimationURL}'),
|
||||
expect(page).toHaveScreenshot({
|
||||
name: 'snapshot.png',
|
||||
animations: "disabled",
|
||||
clip: { x: 0, y: 0, width: 10, height: 10 },
|
||||
}),
|
||||
]);
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should successfully screenshot a page with infinite animation with disableAnimation: true', async ({ runInlineTest }, testInfo) => {
|
||||
const infiniteAnimationURL = pathToFileURL(path.join(__dirname, '../assets/rotate-z.html'));
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await page.goto('${infiniteAnimationURL}');
|
||||
await expect(page).toHaveScreenshot({
|
||||
animations: "disabled",
|
||||
});
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': true });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-1.png'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should support clip option for page', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': createImage(50, 50, 255, 255, 255),
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot({
|
||||
name: 'snapshot.png',
|
||||
clip: { x: 0, y: 0, width: 50, height: 50, },
|
||||
});
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should support omitBackground option for locator', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
document.body.style.setProperty('width', '100px');
|
||||
document.body.style.setProperty('height', '100px');
|
||||
});
|
||||
await expect(page.locator('body')).toHaveScreenshot({
|
||||
name: 'snapshot.png',
|
||||
omitBackground: true,
|
||||
});
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': true });
|
||||
expect(result.exitCode).toBe(0);
|
||||
const snapshotPath = testInfo.outputPath('a.spec.js-snapshots', 'snapshot.png');
|
||||
expect(fs.existsSync(snapshotPath)).toBe(true);
|
||||
const png = PNG.sync.read(fs.readFileSync(snapshotPath));
|
||||
expect.soft(png.width, 'image width must be 100').toBe(100);
|
||||
expect.soft(png.height, 'image height must be 100').toBe(100);
|
||||
expect.soft(png.data[0], 'image R must be 0').toBe(0);
|
||||
expect.soft(png.data[1], 'image G must be 0').toBe(0);
|
||||
expect.soft(png.data[2], 'image B must be 0').toBe(0);
|
||||
expect.soft(png.data[3], 'image A must be 0').toBe(0);
|
||||
});
|
||||
|
||||
test('should fail to screenshot an element with infinite animation', async ({ runInlineTest }, testInfo) => {
|
||||
const infiniteAnimationURL = pathToFileURL(path.join(__dirname, '../assets/rotate-z.html'));
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await page.goto('${infiniteAnimationURL}');
|
||||
await expect(page.locator('body')).toHaveScreenshot({ timeout: 2000 });
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(stripAnsi(result.output)).toContain(`Timeout 2000ms exceeded while generating screenshot because element kept changing`);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.png'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-expected.png'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-diff.png'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-1.png'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should fail to screenshot an element that keeps moving', async ({ runInlineTest }, testInfo) => {
|
||||
const infiniteAnimationURL = pathToFileURL(path.join(__dirname, '../assets/rotate-z.html'));
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await page.goto('${infiniteAnimationURL}');
|
||||
await expect(page.locator('div')).toHaveScreenshot({ timeout: 2000 });
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(stripAnsi(result.output)).toContain(`Timeout 2000ms exceeded`);
|
||||
expect(stripAnsi(result.output)).toContain(`element is not stable - waiting`);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.png'))).toBe(false);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-expected.png'))).toBe(false);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-diff.png'))).toBe(false);
|
||||
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-1.png'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should generate default name', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot();
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.png'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('a.spec.js-snapshots', 'is-a-test-1.png'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should compile with different option combinations', async ({ runTSC }) => {
|
||||
const result = await runTSC({
|
||||
'a.spec.ts': `
|
||||
const { test } = pwt;
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot();
|
||||
await expect(page.locator('body')).toHaveScreenshot({ threshold: 0.2 });
|
||||
await expect(page).toHaveScreenshot({ maxDiffPixelRatio: 0.2 });
|
||||
await expect(page).toHaveScreenshot({
|
||||
threshold: 0.2,
|
||||
maxDiffPixels: 10,
|
||||
maxDiffPixelRatio: 0.2,
|
||||
animations: "disabled",
|
||||
omitBackground: true,
|
||||
timeout: 1000,
|
||||
});
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should fail when screenshot is different size', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': createImage(22, 33),
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output).toContain('Expected an image 22px by 33px, received 1280px by 720px.');
|
||||
});
|
||||
|
||||
test('should fail when screenshot is different pixels', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': paintBlackPixels(whiteImage, 12345),
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output).toContain('Screenshot comparison failed');
|
||||
expect(result.output).toContain('12345 pixels');
|
||||
expect(result.output).not.toContain('Call log');
|
||||
expect(result.output).toContain('ratio 0.02');
|
||||
expect(result.output).toContain('Expected:');
|
||||
expect(result.output).toContain('Received:');
|
||||
});
|
||||
|
||||
test('doesn\'t create comparison artifacts in an output folder for passed negated snapshot matcher', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': blueImage,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).not.toHaveScreenshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
const outputText = stripAnsi(result.output);
|
||||
const expectedSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-expected.png');
|
||||
const actualSnapshotArtifactPath = testInfo.outputPath('test-results', 'a-is-a-test', 'snapshot-actual.png');
|
||||
expect(outputText).not.toContain(`Expected: ${expectedSnapshotArtifactPath}`);
|
||||
expect(outputText).not.toContain(`Received: ${actualSnapshotArtifactPath}`);
|
||||
expect(fs.existsSync(expectedSnapshotArtifactPath)).toBe(false);
|
||||
expect(fs.existsSync(actualSnapshotArtifactPath)).toBe(false);
|
||||
});
|
||||
|
||||
test('should fail on same snapshots with negate matcher', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': whiteImage,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).not.toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output).toContain('Screenshot comparison failed:');
|
||||
expect(result.output).toContain('Expected result should be different from the actual one.');
|
||||
});
|
||||
|
||||
test('should write missing expectations locally twice and continue', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png');
|
||||
await expect(page).toHaveScreenshot('snapshot2.png');
|
||||
console.log('Here we are!');
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
|
||||
const snapshot1OutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||
expect(result.output).toContain(`Error: ${snapshot1OutputPath} is missing in snapshots, writing actual`);
|
||||
expect(pngComparator(fs.readFileSync(snapshot1OutputPath), whiteImage)).toBe(null);
|
||||
|
||||
const snapshot2OutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot2.png');
|
||||
expect(result.output).toContain(`Error: ${snapshot2OutputPath} is missing in snapshots, writing actual`);
|
||||
expect(pngComparator(fs.readFileSync(snapshot2OutputPath), whiteImage)).toBe(null);
|
||||
|
||||
expect(result.output).toContain('Here we are!');
|
||||
|
||||
const stackLines = stripAnsi(result.output).split('\n').filter(line => line.includes(' at ')).filter(line => !line.includes(testInfo.outputPath()));
|
||||
expect(result.output).toContain('a.spec.js:8');
|
||||
expect(stackLines.length).toBe(0);
|
||||
});
|
||||
|
||||
test('shouldn\'t write missing expectations locally for negated matcher', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).not.toHaveScreenshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||
expect(result.output).toContain(`${snapshotOutputPath} is missing in snapshots, matchers using ".not" won\'t write them automatically.`);
|
||||
expect(fs.existsSync(snapshotOutputPath)).toBe(false);
|
||||
});
|
||||
|
||||
test('should update snapshot with the update-snapshots flag', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': blueImage,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': true });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||
expect(result.output).toContain(`${snapshotOutputPath} is re-generated, writing actual.`);
|
||||
expect(pngComparator(fs.readFileSync(snapshotOutputPath), whiteImage)).toBe(null);
|
||||
});
|
||||
|
||||
test('shouldn\'t update snapshot with the update-snapshots flag for negated matcher', async ({ runInlineTest }, testInfo) => {
|
||||
const EXPECTED_SNAPSHOT = blueImage;
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).not.toHaveScreenshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': true });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||
expect(fs.readFileSync(snapshotOutputPath).equals(EXPECTED_SNAPSHOT)).toBe(true);
|
||||
});
|
||||
|
||||
test('should silently write missing expectations locally with the update-snapshots flag', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': true });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||
expect(result.output).toContain(`${snapshotOutputPath} is missing in snapshots, writing actual`);
|
||||
const data = fs.readFileSync(snapshotOutputPath);
|
||||
expect(pngComparator(data, whiteImage)).toBe(null);
|
||||
});
|
||||
|
||||
test('should not write missing expectations locally with the update-snapshots flag for negated matcher', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).not.toHaveScreenshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': true });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||
expect(result.output).toContain(`${snapshotOutputPath} is missing in snapshots, matchers using ".not" won\'t write them automatically.`);
|
||||
expect(fs.existsSync(snapshotOutputPath)).toBe(false);
|
||||
});
|
||||
|
||||
test('should match multiple snapshots', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/red.png': redImage,
|
||||
'a.spec.js-snapshots/green.png': greenImage,
|
||||
'a.spec.js-snapshots/blue.png': blueImage,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await Promise.all([
|
||||
page.evaluate(() => document.documentElement.style.setProperty('background', '#f00')),
|
||||
expect(page).toHaveScreenshot('red.png'),
|
||||
]);
|
||||
await Promise.all([
|
||||
page.evaluate(() => document.documentElement.style.setProperty('background', '#0f0')),
|
||||
expect(page).toHaveScreenshot('green.png'),
|
||||
]);
|
||||
await Promise.all([
|
||||
page.evaluate(() => document.documentElement.style.setProperty('background', '#00f')),
|
||||
expect(page).toHaveScreenshot('blue.png'),
|
||||
]);
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should use provided name', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/provided.png': whiteImage,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('provided.png');
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should use provided name via options', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/provided.png': whiteImage,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot({ name: 'provided.png' });
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should respect maxDiffPixels option', async ({ runInlineTest }) => {
|
||||
const BAD_PIXELS = 120;
|
||||
const EXPECTED_SNAPSHOT = paintBlackPixels(whiteImage, BAD_PIXELS);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure default comparison fails').toBe(1);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png', {
|
||||
maxDiffPixels: ${BAD_PIXELS}
|
||||
});
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure maxDiffPixels option is respected').toBe(0);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'playwright.config.ts': `
|
||||
module.exports = { projects: [
|
||||
{ expect: { toHaveScreenshot: { maxDiffPixels: ${BAD_PIXELS} } } },
|
||||
]};
|
||||
`,
|
||||
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure maxDiffPixels option in project config is respected').toBe(0);
|
||||
});
|
||||
|
||||
test('should satisfy both maxDiffPixelRatio and maxDiffPixels', async ({ runInlineTest }) => {
|
||||
const BAD_RATIO = 0.25;
|
||||
const BAD_COUNT = Math.floor(IMG_WIDTH * IMG_HEIGHT * BAD_RATIO);
|
||||
const EXPECTED_SNAPSHOT = paintBlackPixels(whiteImage, BAD_COUNT);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure default comparison fails').toBe(1);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png', {
|
||||
maxDiffPixels: ${Math.floor(BAD_COUNT / 2)},
|
||||
maxDiffPixelRatio: ${BAD_RATIO},
|
||||
timeout: 2000,
|
||||
});
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure it fails when maxDiffPixels < actualBadPixels < maxDiffPixelRatio').toBe(1);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png', {
|
||||
maxDiffPixels: ${BAD_COUNT},
|
||||
maxDiffPixelRatio: ${BAD_RATIO / 2},
|
||||
timeout: 2000,
|
||||
});
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure it fails when maxDiffPixelRatio < actualBadPixels < maxDiffPixels').toBe(1);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png', {
|
||||
maxDiffPixels: ${BAD_COUNT},
|
||||
maxDiffPixelRatio: ${BAD_RATIO},
|
||||
});
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure it passes when actualBadPixels < maxDiffPixelRatio && actualBadPixels < maxDiffPixels').toBe(0);
|
||||
});
|
||||
|
||||
test('should respect maxDiffPixelRatio option', async ({ runInlineTest }) => {
|
||||
const BAD_RATIO = 0.25;
|
||||
const BAD_PIXELS = IMG_WIDTH * IMG_HEIGHT * BAD_RATIO;
|
||||
const EXPECTED_SNAPSHOT = paintBlackPixels(whiteImage, BAD_PIXELS);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure default comparison fails').toBe(1);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png', {
|
||||
maxDiffPixelRatio: ${BAD_RATIO}
|
||||
});
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure maxDiffPixelRatio option is respected').toBe(0);
|
||||
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'playwright.config.ts': `
|
||||
module.exports = { projects: [
|
||||
{ expect: { toHaveScreenshot: { maxDiffPixelRatio: ${BAD_RATIO} } } },
|
||||
]};
|
||||
`,
|
||||
'a.spec.js-snapshots/snapshot.png': EXPECTED_SNAPSHOT,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
})).exitCode, 'make sure maxDiffPixels option in project config is respected').toBe(0);
|
||||
});
|
||||
|
||||
test('should throw for invalid maxDiffPixels values', async ({ runInlineTest }) => {
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: -1,
|
||||
});
|
||||
});
|
||||
`
|
||||
})).exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test('should throw for invalid maxDiffPixelRatio values', async ({ runInlineTest }) => {
|
||||
expect((await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixelRatio: 12,
|
||||
});
|
||||
});
|
||||
`
|
||||
})).exitCode).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
test('should attach expected/actual and no diff when sizes are different', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'a.spec.js-snapshots/snapshot.png': createImage(2, 2),
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test.afterEach(async ({}, testInfo) => {
|
||||
console.log('## ' + JSON.stringify(testInfo.attachments));
|
||||
});
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png', { timeout: 2000 });
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
const outputText = stripAnsi(result.output);
|
||||
expect(outputText).toContain('Expected an image 2px by 2px, received 1280px by 720px.');
|
||||
const attachments = outputText.split('\n').filter(l => l.startsWith('## ')).map(l => l.substring(3)).map(l => JSON.parse(l))[0];
|
||||
for (const attachment of attachments)
|
||||
attachment.path = attachment.path.replace(/\\/g, '/').replace(/.*test-results\//, '');
|
||||
expect(attachments).toEqual([
|
||||
{
|
||||
name: 'expected',
|
||||
contentType: 'image/png',
|
||||
path: 'a-is-a-test/snapshot-expected.png'
|
||||
},
|
||||
{
|
||||
name: 'actual',
|
||||
contentType: 'image/png',
|
||||
path: 'a-is-a-test/snapshot-actual.png'
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should fail with missing expectations and retries', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'playwright.config.ts': `
|
||||
module.exports = { retries: 1 };
|
||||
`,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||
expect(result.output).toContain(`${snapshotOutputPath} is missing in snapshots, writing actual`);
|
||||
const data = fs.readFileSync(snapshotOutputPath);
|
||||
expect(pngComparator(data, whiteImage)).toBe(null);
|
||||
});
|
||||
|
||||
test('should update expectations with retries', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
...files,
|
||||
'playwright.config.ts': `
|
||||
module.exports = { retries: 1 };
|
||||
`,
|
||||
'a.spec.js': `
|
||||
const { test } = require('./helper');
|
||||
test('is a test', async ({ page }) => {
|
||||
await expect(page).toHaveScreenshot('snapshot.png');
|
||||
});
|
||||
`
|
||||
}, { 'update-snapshots': true });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
const snapshotOutputPath = testInfo.outputPath('a.spec.js-snapshots/snapshot.png');
|
||||
expect(result.output).toContain(`${snapshotOutputPath} is missing in snapshots, writing actual`);
|
||||
const data = fs.readFileSync(snapshotOutputPath);
|
||||
expect(pngComparator(data, whiteImage)).toBe(null);
|
||||
});
|
||||
|
||||
|
|
@ -67,6 +67,13 @@ function build {
|
|||
echo "Unsupported RUN_DRIVER ${RUN_DRIVER}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# NPM install does intentionally set the modification date back to 1985 for all the files. This confuses language binding
|
||||
# update mechanisms, which expect the modification date to be recent to decide which file to override. See:
|
||||
# - https://github.com/npm/npm/issues/20439#issuecomment-385121133
|
||||
# - https://github.com/microsoft/playwright-dotnet/issues/2069
|
||||
find . -type f -exec touch {} +
|
||||
|
||||
zip -q -r ../playwright-${PACKAGE_VERSION}-${SUFFIX}.zip .
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -448,7 +448,11 @@ Documentation.Type = class {
|
|||
* @return {Documentation.Type}
|
||||
*/
|
||||
static fromParsedType(parsedType, inUnion = false) {
|
||||
if (!inUnion && parsedType.union) {
|
||||
if (!inUnion && !parsedType.unionName && isStringUnion(parsedType) ) {
|
||||
throw new Error('Enum must have a name:\n' + JSON.stringify(parsedType, null, 2));
|
||||
}
|
||||
|
||||
if (!inUnion && (parsedType.union || parsedType.unionName)) {
|
||||
const type = new Documentation.Type(parsedType.unionName || '');
|
||||
type.union = [];
|
||||
for (let t = parsedType; t; t = t.union) {
|
||||
|
|
@ -568,8 +572,6 @@ Documentation.Type = class {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
function isStringUnion(type) {
|
||||
if (!type.union)
|
||||
return false;
|
||||
while (type) {
|
||||
if (!type.name.startsWith('"') || !type.name.endsWith('"'))
|
||||
return false;
|
||||
|
|
|
|||
7
utils/generate_types/overrides-test.d.ts
vendored
7
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -42,7 +42,7 @@ type ExpectSettings = {
|
|||
* Default timeout for async expect matchers in milliseconds, defaults to 5000ms.
|
||||
*/
|
||||
timeout?: number;
|
||||
toHaveScreenshot?: {
|
||||
toMatchSnapshot?: {
|
||||
/** An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between pixels in compared images, between zero (strict) and one (lax). Defaults to `0.2`.
|
||||
*/
|
||||
threshold?: number,
|
||||
|
|
@ -55,11 +55,6 @@ type ExpectSettings = {
|
|||
*/
|
||||
maxDiffPixelRatio?: number,
|
||||
}
|
||||
toMatchSnapshot?: {
|
||||
/** An acceptable perceived color difference in the [YIQ color space](https://en.wikipedia.org/wiki/YIQ) between pixels in compared images, between zero (strict) and one (lax). Defaults to `0.2`.
|
||||
*/
|
||||
threshold?: number,
|
||||
}
|
||||
};
|
||||
|
||||
interface TestProject {
|
||||
|
|
|
|||
Loading…
Reference in a new issue