Merge branch 'main' into extend-type-accessible-name

This commit is contained in:
anait-airiian 2024-10-25 20:40:18 +02:00
commit 8224c93cdf
70 changed files with 1194 additions and 2516 deletions

View file

@ -284,3 +284,24 @@ jobs:
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
env: env:
PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW: 1 PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW: 1
test_linux_chromium_headless_shell:
name: Chromium Headless Shell
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy:
fail-fast: false
matrix:
runs-on: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/run-test
with:
browsers-to-install: chromium-headless-shell
command: npm run ctest
bot-name: "headless-shell-${{ matrix.runs-on }}"
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
env:
PWTEST_CHANNEL: chromium-headless-shell

View file

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

View file

@ -479,8 +479,8 @@ export default defineConfig({
## property: TestOptions.screenshot ## property: TestOptions.screenshot
* since: v1.10 * since: v1.10
- type: <[Object]|[ScreenshotMode]<"off"|"on"|"only-on-failure">> - type: <[Object]|[ScreenshotMode]<"off"|"on"|"only-on-failure"|"on-first-failure">>
- `mode` <[ScreenshotMode]<"off"|"on"|"only-on-failure">> Automatic screenshot mode. - `mode` <[ScreenshotMode]<"off"|"on"|"only-on-failure"|"on-first-failure">> Automatic screenshot mode.
- `fullPage` ?<[boolean]> When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to `false`. - `fullPage` ?<[boolean]> When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to `false`.
- `omitBackground` ?<[boolean]> Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images. Defaults to `false`. - `omitBackground` ?<[boolean]> Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images. Defaults to `false`.
@ -488,6 +488,7 @@ Whether to automatically capture a screenshot after each test. Defaults to `'off
* `'off'`: Do not capture screenshots. * `'off'`: Do not capture screenshots.
* `'on'`: Capture screenshot after each test. * `'on'`: Capture screenshot after each test.
* `'only-on-failure'`: Capture screenshot after each test failure. * `'only-on-failure'`: Capture screenshot after each test failure.
* `'on-first-failure'`: Capture screenshot after each test's first failure.
**Usage** **Usage**

View file

@ -99,7 +99,7 @@ See [Running Tests](./running-tests.md) for general information on `pytest` opti
## Examples ## Examples
### Configure Mypy typings for auto-completion ### Configure typings for auto-completion
```py title="test_my_application.py" ```py title="test_my_application.py"
from playwright.sync_api import Page from playwright.sync_api import Page
@ -109,6 +109,8 @@ def test_visit_admin_dashboard(page: Page):
# ... # ...
``` ```
If you're using VSCode with Pylance, these types can be inferred by enabling the `python.testing.pytestEnabled` setting so you don't need the type annotation.
### Configure slow mo ### Configure slow mo
Run tests with slow mo with the `--slowmo` argument. Run tests with slow mo with the `--slowmo` argument.

View file

@ -16,6 +16,7 @@ This project incorporates components from the projects listed below. The origina
- concat-map@0.0.1 (https://github.com/substack/node-concat-map) - concat-map@0.0.1 (https://github.com/substack/node-concat-map)
- debug@4.3.4 (https://github.com/debug-js/debug) - debug@4.3.4 (https://github.com/debug-js/debug)
- define-lazy-prop@2.0.0 (https://github.com/sindresorhus/define-lazy-prop) - define-lazy-prop@2.0.0 (https://github.com/sindresorhus/define-lazy-prop)
- diff-match-patch@1.0.5 (https://github.com/JackuB/diff-match-patch)
- dotenv@16.4.5 (https://github.com/motdotla/dotenv) - dotenv@16.4.5 (https://github.com/motdotla/dotenv)
- end-of-stream@1.4.4 (https://github.com/mafintosh/end-of-stream) - end-of-stream@1.4.4 (https://github.com/mafintosh/end-of-stream)
- escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp) - escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp)
@ -351,6 +352,212 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
========================================= =========================================
END OF define-lazy-prop@2.0.0 AND INFORMATION END OF define-lazy-prop@2.0.0 AND INFORMATION
%% diff-match-patch@1.0.5 NOTICES AND INFORMATION BEGIN HERE
=========================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
=========================================
END OF diff-match-patch@1.0.5 AND INFORMATION
%% dotenv@16.4.5 NOTICES AND INFORMATION BEGIN HERE %% dotenv@16.4.5 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================
Copyright (c) 2015, Scott Motte Copyright (c) 2015, Scott Motte
@ -1194,6 +1401,6 @@ END OF yazl@2.5.1 AND INFORMATION
SUMMARY BEGIN HERE SUMMARY BEGIN HERE
========================================= =========================================
Total Packages: 47 Total Packages: 48
========================================= =========================================
END OF SUMMARY END OF SUMMARY

View file

@ -3,9 +3,9 @@
"browsers": [ "browsers": [
{ {
"name": "chromium", "name": "chromium",
"revision": "1143", "revision": "1145",
"installByDefault": true, "installByDefault": true,
"browserVersion": "131.0.6778.3" "browserVersion": "131.0.6778.13"
}, },
{ {
"name": "chromium-tip-of-tree", "name": "chromium-tip-of-tree",

View file

@ -11,6 +11,7 @@
"colors": "1.4.0", "colors": "1.4.0",
"commander": "8.3.0", "commander": "8.3.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"diff-match-patch": "^1.0.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"graceful-fs": "4.2.10", "graceful-fs": "4.2.10",
"https-proxy-agent": "7.0.5", "https-proxy-agent": "7.0.5",
@ -30,6 +31,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/debug": "^4.1.7", "@types/debug": "^4.1.7",
"@types/diff-match-patch": "^1.0.36",
"@types/mime": "^2.0.3", "@types/mime": "^2.0.3",
"@types/minimatch": "^3.0.5", "@types/minimatch": "^3.0.5",
"@types/pngjs": "^6.0.1", "@types/pngjs": "^6.0.1",
@ -49,6 +51,12 @@
"@types/ms": "*" "@types/ms": "*"
} }
}, },
"node_modules/@types/diff-match-patch": {
"version": "1.0.36",
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
"dev": true
},
"node_modules/@types/mime": { "node_modules/@types/mime": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
@ -201,6 +209,11 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/diff-match-patch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.4.5", "version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
@ -456,6 +469,12 @@
"@types/ms": "*" "@types/ms": "*"
} }
}, },
"@types/diff-match-patch": {
"version": "1.0.36",
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
"dev": true
},
"@types/mime": { "@types/mime": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
@ -587,6 +606,11 @@
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="
}, },
"diff-match-patch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
},
"dotenv": { "dotenv": {
"version": "16.4.5", "version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",

View file

@ -12,6 +12,7 @@
"colors": "1.4.0", "colors": "1.4.0",
"commander": "8.3.0", "commander": "8.3.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"diff-match-patch": "^1.0.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"graceful-fs": "4.2.10", "graceful-fs": "4.2.10",
"https-proxy-agent": "7.0.5", "https-proxy-agent": "7.0.5",
@ -26,11 +27,12 @@
"signal-exit": "3.0.7", "signal-exit": "3.0.7",
"socks-proxy-agent": "8.0.4", "socks-proxy-agent": "8.0.4",
"stack-utils": "2.0.5", "stack-utils": "2.0.5",
"yaml": "^2.5.1", "ws": "8.17.1",
"ws": "8.17.1" "yaml": "^2.5.1"
}, },
"devDependencies": { "devDependencies": {
"@types/debug": "^4.1.7", "@types/debug": "^4.1.7",
"@types/diff-match-patch": "^1.0.36",
"@types/mime": "^2.0.3", "@types/mime": "^2.0.3",
"@types/minimatch": "^3.0.5", "@types/minimatch": "^3.0.5",
"@types/pngjs": "^6.0.1", "@types/pngjs": "^6.0.1",

View file

@ -20,6 +20,9 @@ export const colors = colorsLibrary;
import debugLibrary from 'debug'; import debugLibrary from 'debug';
export const debug = debugLibrary; export const debug = debugLibrary;
import diffMatchPatchLibrary from 'diff-match-patch';
export const diffMatchPatch = diffMatchPatchLibrary;
import dotenvLibrary from 'dotenv'; import dotenvLibrary from 'dotenv';
export const dotenv = dotenvLibrary; export const dotenv = dotenvLibrary;

View file

@ -554,6 +554,7 @@ async function open(options: Options, url: string | undefined, language: string)
contextOptions, contextOptions,
device: options.device, device: options.device,
saveStorage: options.saveStorage, saveStorage: options.saveStorage,
handleSIGINT: false,
}); });
await openPage(context, url); await openPage(context, url);
} }
@ -577,6 +578,7 @@ async function codegen(options: Options & { target: string, output?: string, tes
codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions', codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions',
testIdAttributeName, testIdAttributeName,
outputFile: outputFile ? path.resolve(outputFile) : undefined, outputFile: outputFile ? path.resolve(outputFile) : undefined,
handleSIGINT: false,
}); });
await openPage(context, url); await openPage(context, url);
} }

View file

@ -976,6 +976,7 @@ scheme.BrowserContextEnableRecorderParams = tObject({
device: tOptional(tString), device: tOptional(tString),
saveStorage: tOptional(tString), saveStorage: tOptional(tString),
outputFile: tOptional(tString), outputFile: tOptional(tString),
handleSIGINT: tOptional(tBoolean),
omitCallTracking: tOptional(tBoolean), omitCallTracking: tOptional(tBoolean),
}); });
scheme.BrowserContextEnableRecorderResult = tOptional(tObject({})); scheme.BrowserContextEnableRecorderResult = tOptional(tObject({}));

View file

@ -110,7 +110,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Galaxy S5": { "Galaxy S5": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -121,7 +121,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S5 landscape": { "Galaxy S5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -132,7 +132,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S8": { "Galaxy S8": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 740 "height": 740
@ -143,7 +143,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S8 landscape": { "Galaxy S8 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 740, "width": 740,
"height": 360 "height": 360
@ -154,7 +154,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S9+": { "Galaxy S9+": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 320, "width": 320,
"height": 658 "height": 658
@ -165,7 +165,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S9+ landscape": { "Galaxy S9+ landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 658, "width": 658,
"height": 320 "height": 320
@ -176,7 +176,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy Tab S4": { "Galaxy Tab S4": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
"viewport": { "viewport": {
"width": 712, "width": 712,
"height": 1138 "height": 1138
@ -187,7 +187,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy Tab S4 landscape": { "Galaxy Tab S4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
"viewport": { "viewport": {
"width": 1138, "width": 1138,
"height": 712 "height": 712
@ -1098,7 +1098,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"LG Optimus L70": { "LG Optimus L70": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 384, "width": 384,
"height": 640 "height": 640
@ -1109,7 +1109,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"LG Optimus L70 landscape": { "LG Optimus L70 landscape": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 384 "height": 384
@ -1120,7 +1120,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 550": { "Microsoft Lumia 550": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1131,7 +1131,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 550 landscape": { "Microsoft Lumia 550 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1142,7 +1142,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 950": { "Microsoft Lumia 950": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1153,7 +1153,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 950 landscape": { "Microsoft Lumia 950 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1164,7 +1164,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 10": { "Nexus 10": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
"viewport": { "viewport": {
"width": 800, "width": 800,
"height": 1280 "height": 1280
@ -1175,7 +1175,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 10 landscape": { "Nexus 10 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
"viewport": { "viewport": {
"width": 1280, "width": 1280,
"height": 800 "height": 800
@ -1186,7 +1186,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 4": { "Nexus 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 384, "width": 384,
"height": 640 "height": 640
@ -1197,7 +1197,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 4 landscape": { "Nexus 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 384 "height": 384
@ -1208,7 +1208,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5": { "Nexus 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1219,7 +1219,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5 landscape": { "Nexus 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1230,7 +1230,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5X": { "Nexus 5X": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1241,7 +1241,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5X landscape": { "Nexus 5X landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1252,7 +1252,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6": { "Nexus 6": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1263,7 +1263,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6 landscape": { "Nexus 6 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1274,7 +1274,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6P": { "Nexus 6P": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1285,7 +1285,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6P landscape": { "Nexus 6P landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1296,7 +1296,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 7": { "Nexus 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
"viewport": { "viewport": {
"width": 600, "width": 600,
"height": 960 "height": 960
@ -1307,7 +1307,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 7 landscape": { "Nexus 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
"viewport": { "viewport": {
"width": 960, "width": 960,
"height": 600 "height": 600
@ -1362,7 +1362,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Pixel 2": { "Pixel 2": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 411, "width": 411,
"height": 731 "height": 731
@ -1373,7 +1373,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 landscape": { "Pixel 2 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 731, "width": 731,
"height": 411 "height": 411
@ -1384,7 +1384,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 XL": { "Pixel 2 XL": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 411, "width": 411,
"height": 823 "height": 823
@ -1395,7 +1395,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 XL landscape": { "Pixel 2 XL landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 823, "width": 823,
"height": 411 "height": 411
@ -1406,7 +1406,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 3": { "Pixel 3": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 393, "width": 393,
"height": 786 "height": 786
@ -1417,7 +1417,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 3 landscape": { "Pixel 3 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 786, "width": 786,
"height": 393 "height": 393
@ -1428,7 +1428,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4": { "Pixel 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 353, "width": 353,
"height": 745 "height": 745
@ -1439,7 +1439,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4 landscape": { "Pixel 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 745, "width": 745,
"height": 353 "height": 353
@ -1450,7 +1450,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4a (5G)": { "Pixel 4a (5G)": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"screen": { "screen": {
"width": 412, "width": 412,
"height": 892 "height": 892
@ -1465,7 +1465,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4a (5G) landscape": { "Pixel 4a (5G) landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"screen": { "screen": {
"height": 892, "height": 892,
"width": 412 "width": 412
@ -1480,7 +1480,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 5": { "Pixel 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"screen": { "screen": {
"width": 393, "width": 393,
"height": 851 "height": 851
@ -1495,7 +1495,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 5 landscape": { "Pixel 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"screen": { "screen": {
"width": 851, "width": 851,
"height": 393 "height": 393
@ -1510,7 +1510,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 7": { "Pixel 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"screen": { "screen": {
"width": 412, "width": 412,
"height": 915 "height": 915
@ -1525,7 +1525,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 7 landscape": { "Pixel 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"screen": { "screen": {
"width": 915, "width": 915,
"height": 412 "height": 412
@ -1540,7 +1540,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Moto G4": { "Moto G4": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1551,7 +1551,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Moto G4 landscape": { "Moto G4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1562,7 +1562,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Chrome HiDPI": { "Desktop Chrome HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
"screen": { "screen": {
"width": 1792, "width": 1792,
"height": 1120 "height": 1120
@ -1577,7 +1577,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Edge HiDPI": { "Desktop Edge HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36 Edg/131.0.6778.3", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36 Edg/131.0.6778.13",
"screen": { "screen": {
"width": 1792, "width": 1792,
"height": 1120 "height": 1120
@ -1622,7 +1622,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Desktop Chrome": { "Desktop Chrome": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080
@ -1637,7 +1637,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Edge": { "Desktop Edge": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.3 Safari/537.36 Edg/131.0.6778.3", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.13 Safari/537.36 Edg/131.0.6778.13",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080

View file

@ -14,9 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { escapeWithQuotes } from '@isomorphic/stringUtils';
import * as roleUtils from './roleUtils'; import * as roleUtils from './roleUtils';
import { isElementVisible, isElementStyleVisibilityVisible, getElementComputedStyle } from './domUtils'; import { getElementComputedStyle } from './domUtils';
import type { AriaRole } from './roleUtils'; import type { AriaRole } from './roleUtils';
type AriaProps = { type AriaProps = {
@ -29,7 +28,7 @@ type AriaProps = {
}; };
type AriaNode = AriaProps & { type AriaNode = AriaProps & {
role: AriaRole | 'fragment' | 'text'; role: AriaRole | 'fragment';
name: string; name: string;
children: (AriaNode | string)[]; children: (AriaNode | string)[];
}; };
@ -56,22 +55,10 @@ export function generateAriaTree(rootElement: Element): AriaNode {
if (roleUtils.isElementHiddenForAria(element)) if (roleUtils.isElementHiddenForAria(element))
return; return;
const visible = isElementVisible(element);
const hasVisibleChildren = isElementStyleVisibilityVisible(element);
if (!hasVisibleChildren)
return;
if (visible) {
const childAriaNode = toAriaNode(element); const childAriaNode = toAriaNode(element);
const isHiddenContainer = childAriaNode && hiddenContainerRoles.has(childAriaNode.ariaNode.role); if (childAriaNode)
if (childAriaNode && !isHiddenContainer) ariaNode.children.push(childAriaNode);
ariaNode.children.push(childAriaNode.ariaNode); processChildNodes(childAriaNode || ariaNode, element);
if (isHiddenContainer || !childAriaNode?.isLeaf)
processChildNodes(childAriaNode?.ariaNode || ariaNode, element);
} else {
processChildNodes(ariaNode, element);
}
}; };
function processChildNodes(ariaNode: AriaNode, element: Element) { function processChildNodes(ariaNode: AriaNode, element: Element) {
@ -101,6 +88,9 @@ export function generateAriaTree(rootElement: Element): AriaNode {
if (treatAsBlock) if (treatAsBlock)
ariaNode.children.push(treatAsBlock); ariaNode.children.push(treatAsBlock);
if (ariaNode.children.length === 1 && ariaNode.name === ariaNode.children[0])
ariaNode.children = [];
} }
roleUtils.beginAriaCaches(); roleUtils.beginAriaCaches();
@ -115,19 +105,13 @@ export function generateAriaTree(rootElement: Element): AriaNode {
return ariaRoot; return ariaRoot;
} }
function toAriaNode(element: Element): { ariaNode: AriaNode, isLeaf: boolean } | null { function toAriaNode(element: Element): AriaNode | null {
const role = roleUtils.getAriaRole(element); const role = roleUtils.getAriaRole(element);
if (!role) if (!role)
return null; return null;
const name = roleUtils.getElementAccessibleName(element, false) || ''; const name = roleUtils.getElementAccessibleName(element, false) || '';
const isLeaf = leafRoles.has(role);
const result: AriaNode = { role, name, children: [] }; const result: AriaNode = { role, name, children: [] };
if (isLeaf && !name) {
const text = roleUtils.accumulatedElementText(element);
if (text)
result.children = [text];
}
if (roleUtils.kAriaCheckedRoles.includes(role)) if (roleUtils.kAriaCheckedRoles.includes(role))
result.checked = roleUtils.getAriaChecked(element); result.checked = roleUtils.getAriaChecked(element);
@ -147,7 +131,7 @@ function toAriaNode(element: Element): { ariaNode: AriaNode, isLeaf: boolean } |
if (roleUtils.kAriaSelectedRoles.includes(role)) if (roleUtils.kAriaSelectedRoles.includes(role))
result.selected = roleUtils.getAriaSelected(element); result.selected = roleUtils.getAriaSelected(element);
return { isLeaf, ariaNode: result }; return result;
} }
export function renderedAriaTree(rootElement: Element): string { export function renderedAriaTree(rootElement: Element): string {
@ -178,21 +162,12 @@ function normalizeStringChildren(rootA11yNode: AriaNode) {
} }
flushChildren(buffer, normalizedChildren); flushChildren(buffer, normalizedChildren);
ariaNode.children = normalizedChildren.length ? normalizedChildren : []; ariaNode.children = normalizedChildren.length ? normalizedChildren : [];
if (ariaNode.children.length === 1 && ariaNode.children[0] === ariaNode.name)
ariaNode.children = [];
}; };
visit(rootA11yNode); visit(rootA11yNode);
} }
const hiddenContainerRoles = new Set(['none', 'presentation']);
const leafRoles = new Set<AriaRole>([
'alert', 'blockquote', 'button', 'caption', 'checkbox', 'code', 'columnheader',
'definition', 'deletion', 'emphasis', 'generic', 'heading', 'img', 'insertion',
'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'meter', 'option',
'progressbar', 'radio', 'rowheader', 'scrollbar', 'searchbox', 'separator',
'slider', 'spinbutton', 'strong', 'subscript', 'superscript', 'switch', 'tab', 'term',
'textbox', 'time', 'tooltip'
]);
const normalizeWhitespaceWithin = (text: string) => text.replace(/[\s\t\r\n]+/g, ' '); const normalizeWhitespaceWithin = (text: string) => text.replace(/[\s\t\r\n]+/g, ' ');
function matchesText(text: string | undefined, template: RegExp | string | undefined) { function matchesText(text: string | undefined, template: RegExp | string | undefined) {
@ -208,7 +183,7 @@ function matchesText(text: string | undefined, template: RegExp | string | undef
export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: string } { export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: string } {
const root = generateAriaTree(rootElement); const root = generateAriaTree(rootElement);
const matches = matchesNodeDeep(root, template); const matches = matchesNodeDeep(root, template);
return { matches, received: renderAriaTree(root, { noText: true }) }; return { matches, received: renderAriaTree(root) };
} }
function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegExp | string, depth: number): boolean { function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegExp | string, depth: number): boolean {
@ -276,17 +251,16 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode): boolean {
return !!results.length; return !!results.length;
} }
export function renderAriaTree(ariaNode: AriaNode, options?: { noText?: boolean }): string { export function renderAriaTree(ariaNode: AriaNode): string {
const lines: string[] = []; const lines: string[] = [];
const visit = (ariaNode: AriaNode | string, indent: string) => { const visit = (ariaNode: AriaNode | string, indent: string) => {
if (typeof ariaNode === 'string') { if (typeof ariaNode === 'string') {
if (!options?.noText)
lines.push(indent + '- text: ' + quoteYamlString(ariaNode)); lines.push(indent + '- text: ' + quoteYamlString(ariaNode));
return; return;
} }
let line = `${indent}- ${ariaNode.role}`; let line = `${indent}- ${ariaNode.role}`;
if (ariaNode.name) if (ariaNode.name)
line += ` ${escapeWithQuotes(ariaNode.name, '"')}`; line += ` ${quoteYamlString(ariaNode.name)}`;
if (ariaNode.checked === 'mixed') if (ariaNode.checked === 'mixed')
line += ` [checked=mixed]`; line += ` [checked=mixed]`;
@ -305,17 +279,16 @@ export function renderAriaTree(ariaNode: AriaNode, options?: { noText?: boolean
if (ariaNode.selected === true) if (ariaNode.selected === true)
line += ` [selected]`; line += ` [selected]`;
const stringValue = !ariaNode.children.length || (ariaNode.children?.length === 1 && typeof ariaNode.children[0] === 'string'); if (!ariaNode.children.length) {
if (stringValue) {
if (!options?.noText && ariaNode.children.length)
line += ': ' + quoteYamlString(ariaNode.children?.[0] as string);
lines.push(line); lines.push(line);
return; } else if (ariaNode.children.length === 1 && typeof ariaNode.children[0] === 'string') {
} line += ': ' + quoteYamlString(ariaNode.children[0]);
lines.push(line);
} else {
lines.push(line + ':'); lines.push(line + ':');
for (const child of ariaNode.children || []) for (const child of ariaNode.children || [])
visit(child, indent + ' '); visit(child, indent + ' ');
}
}; };
if (ariaNode.role === 'fragment') { if (ariaNode.role === 'fragment') {

View file

@ -34,6 +34,7 @@ import { buildFullSelector } from '../utils/isomorphic/recorderUtils';
const recorderSymbol = Symbol('recorderSymbol'); const recorderSymbol = Symbol('recorderSymbol');
export class Recorder implements InstrumentationListener, IRecorder { export class Recorder implements InstrumentationListener, IRecorder {
readonly handleSIGINT: boolean | undefined;
private _context: BrowserContext; private _context: BrowserContext;
private _mode: Mode; private _mode: Mode;
private _highlightedSelector = ''; private _highlightedSelector = '';
@ -75,6 +76,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) { constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
this._mode = params.mode || 'none'; this._mode = params.mode || 'none';
this.handleSIGINT = params.handleSIGINT;
this._contextRecorder = new ContextRecorder(codegenMode, context, params, {}); this._contextRecorder = new ContextRecorder(codegenMode, context, params, {});
this._context = context; this._context = context;
this._omitCallTracking = !!params.omitCallTracking; this._omitCallTracking = !!params.omitCallTracking;

View file

@ -111,7 +111,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
noDefaultViewport: true, noDefaultViewport: true,
headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed), headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed),
useWebSocket: isUnderTest(), useWebSocket: isUnderTest(),
handleSIGINT: false, handleSIGINT: recorder.handleSIGINT,
executablePath: inspectedContext._browser.options.isChromium ? inspectedContext._browser.options.customExecutablePath : undefined, executablePath: inspectedContext._browser.options.isChromium ? inspectedContext._browser.options.customExecutablePath : undefined,
} }
}); });

View file

@ -21,6 +21,7 @@ import type { EventEmitter } from 'events';
export interface IRecorder { export interface IRecorder {
setMode(mode: Mode): void; setMode(mode: Mode): void;
mode(): Mode; mode(): Mode;
readonly handleSIGINT: boolean | undefined;
} }
export interface IRecorderApp extends EventEmitter { export interface IRecorderApp extends EventEmitter {

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
[*] [*]
./ ./
../third_party/diff_match_patch
../third_party/pixelmatch ../third_party/pixelmatch
../image_tools/compare.ts ../image_tools/compare.ts
../utilsBundle.ts ../utilsBundle.ts

View file

@ -18,7 +18,7 @@
import { colors, jpegjs } from '../utilsBundle'; import { colors, jpegjs } from '../utilsBundle';
const pixelmatch = require('../third_party/pixelmatch'); const pixelmatch = require('../third_party/pixelmatch');
import { compare } from '../image_tools/compare'; import { compare } from '../image_tools/compare';
const { diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL } = require('../third_party/diff_match_patch'); const { diffMatchPatch } = require('../utilsBundle');
import { PNG } from '../utilsBundle'; import { PNG } from '../utilsBundle';
export type ImageComparatorOptions = { threshold?: number, maxDiffPixels?: number, maxDiffPixelRatio?: number, comparator?: string }; export type ImageComparatorOptions = { threshold?: number, maxDiffPixels?: number, maxDiffPixelRatio?: number, comparator?: string };
@ -106,6 +106,7 @@ function validateBuffer(buffer: Buffer, mimeType: string): void {
} }
function compareText(actual: Buffer | string, expectedBuffer: Buffer): ComparatorResult { function compareText(actual: Buffer | string, expectedBuffer: Buffer): ComparatorResult {
const { diff_match_patch } = diffMatchPatch;
if (typeof actual !== 'string') if (typeof actual !== 'string')
return { errorMessage: 'Actual result should be a string' }; return { errorMessage: 'Actual result should be a string' };
const expected = expectedBuffer.toString('utf-8'); const expected = expectedBuffer.toString('utf-8');
@ -120,6 +121,7 @@ function compareText(actual: Buffer | string, expectedBuffer: Buffer): Comparato
} }
function diff_prettyTerminal(diffs: [number, string][]) { function diff_prettyTerminal(diffs: [number, string][]) {
const { DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL } = diffMatchPatch;
const html = []; const html = [];
for (let x = 0; x < diffs.length; x++) { for (let x = 0; x < diffs.length; x++) {
const op = diffs[x][0]; // Operation (insert, delete, equal) const op = diffs[x][0]; // Operation (insert, delete, equal)

View file

@ -33,6 +33,7 @@ export * from './isomorphic/stringUtils';
export * from './isomorphic/urlMatch'; export * from './isomorphic/urlMatch';
export * from './multimap'; export * from './multimap';
export * from './network'; export * from './network';
export * from './patch';
export * from './processLauncher'; export * from './processLauncher';
export * from './profiler'; export * from './profiler';
export * from './rtti'; export * from './rtti';

View file

@ -19,7 +19,7 @@ import { type NestedSelectorBody, parseAttributeSelector, parseSelector, stringi
import type { ParsedSelector } from './selectorParser'; import type { ParsedSelector } from './selectorParser';
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl'; export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl';
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has-not-text' | 'has' | 'hasNot' | 'frame' | 'and' | 'or' | 'chain'; export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has-not-text' | 'has' | 'hasNot' | 'frame' | 'frame-locator' | 'and' | 'or' | 'chain';
export type LocatorBase = 'page' | 'locator' | 'frame-locator'; export type LocatorBase = 'page' | 'locator' | 'frame-locator';
export type Quote = '\'' | '"' | '`'; export type Quote = '\'' | '"' | '`';
@ -158,19 +158,29 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
} }
} }
if (part.name === 'internal:control' && (part.body as string) === 'enter-frame') { if (part.name === 'internal:control' && (part.body as string) === 'enter-frame') {
tokens.push([factory.generateLocator(base, 'frame', '')]); // transform last tokens from `${selector}` into `${selector}.contentFrame()` and `frameLocator(${selector})`
const lastTokens = tokens[tokens.length - 1];
const lastPart = parts[index - 1];
const transformed = lastTokens.map(token => factory.chainLocators([token, factory.generateLocator(base, 'frame', '')]));
if (['xpath', 'css'].includes(lastPart.name)) {
transformed.push(
factory.generateLocator(base, 'frame-locator', stringifySelector({ parts: [lastPart] })),
factory.generateLocator(base, 'frame-locator', stringifySelector({ parts: [lastPart] }, true))
);
}
lastTokens.splice(0, lastTokens.length, ...transformed);
nextBase = 'frame-locator'; nextBase = 'frame-locator';
continue; continue;
} }
const locatorType: LocatorType = 'default';
const nextPart = parts[index + 1]; const nextPart = parts[index + 1];
const selectorPart = stringifySelector({ parts: [part] }); const selectorPart = stringifySelector({ parts: [part] });
const locatorPart = factory.generateLocator(base, locatorType, selectorPart); const locatorPart = factory.generateLocator(base, 'default', selectorPart);
if (locatorType === 'default' && nextPart && ['internal:has-text', 'internal:has-not-text'].includes(nextPart.name)) { if (nextPart && ['internal:has-text', 'internal:has-not-text'].includes(nextPart.name)) {
const { exact, text } = detectExact(nextPart.body as string); const { exact, text } = detectExact(nextPart.body as string);
// There is no locator equivalent for strict has-text and has-not-text, leave it as is. // There is no locator equivalent for strict has-text and has-not-text, leave it as is.
if (!exact) { if (!exact) {
@ -194,7 +204,7 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
let locatorPartWithEngine: string | undefined; let locatorPartWithEngine: string | undefined;
if (['xpath', 'css'].includes(part.name)) { if (['xpath', 'css'].includes(part.name)) {
const selectorPart = stringifySelector({ parts: [part] }, /* forceEngineName */ true); const selectorPart = stringifySelector({ parts: [part] }, /* forceEngineName */ true);
locatorPartWithEngine = factory.generateLocator(base, locatorType, selectorPart); locatorPartWithEngine = factory.generateLocator(base, 'default', selectorPart);
} }
tokens.push([locatorPart, locatorPartWithEngine].filter(Boolean) as string[]); tokens.push([locatorPart, locatorPartWithEngine].filter(Boolean) as string[]);
@ -253,6 +263,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
if (options.hasNotText !== undefined) if (options.hasNotText !== undefined)
return `locator(${this.quote(body as string)}, { hasNotText: ${this.toHasText(options.hasNotText)} })`; return `locator(${this.quote(body as string)}, { hasNotText: ${this.toHasText(options.hasNotText)} })`;
return `locator(${this.quote(body as string)})`; return `locator(${this.quote(body as string)})`;
case 'frame-locator':
return `frameLocator(${this.quote(body as string)})`;
case 'frame': case 'frame':
return `contentFrame()`; return `contentFrame()`;
case 'nth': case 'nth':
@ -345,6 +357,8 @@ export class PythonLocatorFactory implements LocatorFactory {
if (options.hasNotText !== undefined) if (options.hasNotText !== undefined)
return `locator(${this.quote(body as string)}, has_not_text=${this.toHasText(options.hasNotText)})`; return `locator(${this.quote(body as string)}, has_not_text=${this.toHasText(options.hasNotText)})`;
return `locator(${this.quote(body as string)})`; return `locator(${this.quote(body as string)})`;
case 'frame-locator':
return `frame_locator(${this.quote(body as string)})`;
case 'frame': case 'frame':
return `content_frame`; return `content_frame`;
case 'nth': case 'nth':
@ -450,6 +464,8 @@ export class JavaLocatorFactory implements LocatorFactory {
if (options.hasNotText !== undefined) if (options.hasNotText !== undefined)
return `locator(${this.quote(body as string)}, new ${clazz}.LocatorOptions().setHasNotText(${this.toHasText(options.hasNotText)}))`; return `locator(${this.quote(body as string)}, new ${clazz}.LocatorOptions().setHasNotText(${this.toHasText(options.hasNotText)}))`;
return `locator(${this.quote(body as string)})`; return `locator(${this.quote(body as string)})`;
case 'frame-locator':
return `frameLocator(${this.quote(body as string)})`;
case 'frame': case 'frame':
return `contentFrame()`; return `contentFrame()`;
case 'nth': case 'nth':
@ -545,6 +561,8 @@ export class CSharpLocatorFactory implements LocatorFactory {
if (options.hasNotText !== undefined) if (options.hasNotText !== undefined)
return `Locator(${this.quote(body as string)}, new() { ${this.toHasNotText(options.hasNotText)} })`; return `Locator(${this.quote(body as string)}, new() { ${this.toHasNotText(options.hasNotText)} })`;
return `Locator(${this.quote(body as string)})`; return `Locator(${this.quote(body as string)})`;
case 'frame-locator':
return `FrameLocator(${this.quote(body as string)})`;
case 'frame': case 'frame':
return `ContentFrame`; return `ContentFrame`;
case 'nth': case 'nth':

View file

@ -0,0 +1,127 @@
/**
* 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 { diffMatchPatch } from '../utilsBundle';
type Hunk = {
lines: string[];
startNew: number;
startOld: number;
contextBefore: number;
contextAfter: number;
};
export function generateUnifiedDiff(text1: string, text2: string, relativeName: string = 'file'): string {
const { diff_match_patch, DIFF_EQUAL, DIFF_DELETE, DIFF_INSERT } = diffMatchPatch;
const dmp = new diff_match_patch();
const a = text1.replace(/\r\n/g, '\n');
const b = text2.replace(/\r\n/g, '\n');
const { chars1, chars2, lineArray } = dmp.diff_linesToChars_(a, b);
const diffs = dmp.diff_main(chars1, chars2, false);
dmp.diff_charsToLines_(diffs, lineArray);
const contextSize = 3;
const hunks: Hunk[] = [];
let lineOld = 1;
let lineNew = 1;
let hunk: Hunk | null = null;
let contextBuffer: string[] = [];
for (const diff of diffs) {
const op = diff[0];
const data = diff[1];
const lines = data.split('\n');
// Remove the last empty line if data ends with '\n'
if (lines[lines.length - 1] === '')
lines.pop();
for (const line of lines) {
if (op === DIFF_EQUAL) {
if (hunk) {
hunk.lines.push(' ' + line);
hunk.contextAfter++;
if (hunk.contextAfter >= contextSize) {
// Close the hunk
hunks.push(hunk);
hunk = null;
contextBuffer = [];
}
} else {
contextBuffer.push(' ' + line);
if (contextBuffer.length > contextSize)
contextBuffer.shift();
}
lineOld++;
lineNew++;
} else {
if (!hunk) {
// Start a new hunk
const hunkStartOld = lineOld - contextBuffer.length;
const hunkStartNew = lineNew - contextBuffer.length;
hunk = {
startOld: hunkStartOld,
startNew: hunkStartNew,
lines: [...contextBuffer],
contextBefore: contextBuffer.length,
contextAfter: 0,
};
}
hunk.contextAfter = 0;
if (op === DIFF_DELETE) {
hunk.lines.push('-' + line);
lineOld++;
} else if (op === DIFF_INSERT) {
hunk.lines.push('+' + line);
lineNew++;
}
}
}
}
if (hunk)
hunks.push(hunk);
// Build the unified diff text
let diffText = `--- a/${relativeName}\n+++ b/${relativeName}\n`;
for (const hunk of hunks) {
// Calculate hunk ranges
const oldRangeStart = hunk.startOld;
const newRangeStart = hunk.startNew;
let oldRangeLines = 0;
let newRangeLines = 0;
for (const line of hunk.lines) {
if (line.startsWith('-') || line.startsWith(' '))
oldRangeLines++;
if (line.startsWith('+') || line.startsWith(' '))
newRangeLines++;
}
// Adjust starting line numbers when range is empty
const oldStartLine = oldRangeLines === 0 ? oldRangeStart - 1 : oldRangeStart;
const newStartLine = newRangeLines === 0 ? newRangeStart - 1 : newRangeStart;
diffText += `@@ -${oldStartLine},${oldRangeLines} +${newStartLine},${newRangeLines} @@\n`;
diffText += hunk.lines.map(line => line + '\n').join('');
}
return diffText;
}

View file

@ -19,6 +19,7 @@ import path from 'path';
export const colors: typeof import('../bundles/utils/node_modules/colors/safe') = require('./utilsBundleImpl').colors; export const colors: typeof import('../bundles/utils/node_modules/colors/safe') = require('./utilsBundleImpl').colors;
export const debug: typeof import('../bundles/utils/node_modules/@types/debug') = require('./utilsBundleImpl').debug; export const debug: typeof import('../bundles/utils/node_modules/@types/debug') = require('./utilsBundleImpl').debug;
export const diffMatchPatch: typeof import('../bundles/utils/node_modules/@types/diff-match-patch') = require('./utilsBundleImpl').diffMatchPatch;
export const dotenv: typeof import('../bundles/utils/node_modules/dotenv') = require('./utilsBundleImpl').dotenv; export const dotenv: typeof import('../bundles/utils/node_modules/dotenv') = require('./utilsBundleImpl').dotenv;
export const getProxyForUrl: typeof import('../bundles/utils/node_modules/@types/proxy-from-env').getProxyForUrl = require('./utilsBundleImpl').getProxyForUrl; export const getProxyForUrl: typeof import('../bundles/utils/node_modules/@types/proxy-from-env').getProxyForUrl = require('./utilsBundleImpl').getProxyForUrl;
export const HttpsProxyAgent: typeof import('../bundles/utils/node_modules/https-proxy-agent').HttpsProxyAgent = require('./utilsBundleImpl').HttpsProxyAgent; export const HttpsProxyAgent: typeof import('../bundles/utils/node_modules/https-proxy-agent').HttpsProxyAgent = require('./utilsBundleImpl').HttpsProxyAgent;
@ -42,12 +43,8 @@ import type { StackFrame } from '@protocol/channels';
const StackUtils: typeof import('../bundles/utils/node_modules/@types/stack-utils') = require('./utilsBundleImpl').StackUtils; const StackUtils: typeof import('../bundles/utils/node_modules/@types/stack-utils') = require('./utilsBundleImpl').StackUtils;
const stackUtils = new StackUtils({ internals: StackUtils.nodeInternals() }); const stackUtils = new StackUtils({ internals: StackUtils.nodeInternals() });
const nodeInternals = StackUtils.nodeInternals();
const nodeMajorVersion = +process.versions.node.split('.')[0];
export function parseStackTraceLine(line: string): StackFrame | null { export function parseStackTraceLine(line: string): StackFrame | null {
if (!process.env.PWDEBUGIMPL && nodeMajorVersion < 16 && nodeInternals.some(internal => internal.test(line)))
return null;
const frame = stackUtils.parseLine(line); const frame = stackUtils.parseLine(line);
if (!frame) if (!frame)
return null; return null;

View file

@ -11,6 +11,7 @@
"@babel/code-frame": "^7.24.2", "@babel/code-frame": "^7.24.2",
"@babel/core": "^7.24.4", "@babel/core": "^7.24.4",
"@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-plugin-utils": "^7.24.0",
"@babel/parser": "^7.24.4",
"@babel/plugin-proposal-decorators": "^7.24.1", "@babel/plugin-proposal-decorators": "^7.24.1",
"@babel/plugin-proposal-explicit-resource-management": "^7.24.1", "@babel/plugin-proposal-explicit-resource-management": "^7.24.1",
"@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-async-generators": "^7.8.4",

View file

@ -12,6 +12,7 @@
"@babel/code-frame": "^7.24.2", "@babel/code-frame": "^7.24.2",
"@babel/core": "^7.24.4", "@babel/core": "^7.24.4",
"@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-plugin-utils": "^7.24.0",
"@babel/parser": "^7.24.4",
"@babel/plugin-proposal-decorators": "^7.24.1", "@babel/plugin-proposal-decorators": "^7.24.1",
"@babel/plugin-proposal-explicit-resource-management": "^7.24.1", "@babel/plugin-proposal-explicit-resource-management": "^7.24.1",
"@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-async-generators": "^7.8.4",

View file

@ -23,6 +23,7 @@ import * as babel from '@babel/core';
export { codeFrameColumns } from '@babel/code-frame'; export { codeFrameColumns } from '@babel/code-frame';
export { declare } from '@babel/helper-plugin-utils'; export { declare } from '@babel/helper-plugin-utils';
export { types } from '@babel/core'; export { types } from '@babel/core';
export { parse } from '@babel/parser';
import traverseFunction from '@babel/traverse'; import traverseFunction from '@babel/traverse';
export const traverse = traverseFunction; export const traverse = traverseFunction;

View file

@ -378,11 +378,6 @@ export function restartWithExperimentalTsEsm(configFile: string | undefined, for
// Now check for the newer API presence. // Now check for the newer API presence.
if (!require('node:module').register) { if (!require('node:module').register) {
// Older API is experimental, only supported on Node 16+.
const nodeVersion = +process.versions.node.split('.')[0];
if (nodeVersion < 16)
return false;
// With older API requiring a process restart, do so conditionally on the config. // With older API requiring a process restart, do so conditionally on the config.
const configIsModule = !!configFile && fileIsModule(configFile); const configIsModule = !!configFile && fileIsModule(configFile);
if (!force && !configIsModule) if (!force && !configIsModule)

View file

@ -15,9 +15,11 @@
*/ */
import util from 'util'; import util from 'util';
import { type SerializedCompilationCache, serializeCompilationCache } from '../transform/compilationCache'; import { serializeCompilationCache } from '../transform/compilationCache';
import type { SerializedCompilationCache } from '../transform/compilationCache';
import type { ConfigLocation, FullConfigInternal } from './config'; import type { ConfigLocation, FullConfigInternal } from './config';
import type { ReporterDescription, TestInfoError, TestStatus } from '../../types/test'; import type { ReporterDescription, TestInfoError, TestStatus } from '../../types/test';
import type { MatcherResultProperty } from '../matchers/matcherHint';
export type ConfigCLIOverrides = { export type ConfigCLIOverrides = {
debug?: boolean; debug?: boolean;
@ -74,11 +76,15 @@ export type AttachmentPayload = {
contentType: string; contentType: string;
}; };
export type TestInfoErrorImpl = TestInfoError & {
matcherResult?: MatcherResultProperty;
};
export type TestEndPayload = { export type TestEndPayload = {
testId: string; testId: string;
duration: number; duration: number;
status: TestStatus; status: TestStatus;
errors: TestInfoError[]; errors: TestInfoErrorImpl[];
hasNonRetriableError: boolean; hasNonRetriableError: boolean;
expectedStatus: TestStatus; expectedStatus: TestStatus;
annotations: { type: string, description?: string }[]; annotations: { type: string, description?: string }[];
@ -99,7 +105,8 @@ export type StepEndPayload = {
testId: string; testId: string;
stepId: string; stepId: string;
wallTime: number; // milliseconds since unix epoch wallTime: number; // milliseconds since unix epoch
error?: TestInfoError; error?: TestInfoErrorImpl;
suggestedRebaseline?: string;
}; };
export type TestEntry = { export type TestEntry = {
@ -113,7 +120,7 @@ export type RunPayload = {
}; };
export type DonePayload = { export type DonePayload = {
fatalErrors: TestInfoError[]; fatalErrors: TestInfoErrorImpl[];
skipTestsDueToSetupFailure: string[]; // test ids skipTestsDueToSetupFailure: string[]; // test ids
fatalUnknownTestIds?: string[]; fatalUnknownTestIds?: string[];
}; };
@ -124,7 +131,7 @@ export type TestOutputPayload = {
}; };
export type TeardownErrorsPayload = { export type TeardownErrorsPayload = {
fatalErrors: TestInfoError[]; fatalErrors: TestInfoErrorImpl[];
}; };
export type EnvProducedPayload = [string, string | null][]; export type EnvProducedPayload = [string, string | null][];

View file

@ -14,9 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import type { EnvProducedPayload, ProcessInitParams } from './ipc'; import type { EnvProducedPayload, ProcessInitParams, TestInfoErrorImpl } from './ipc';
import { startProfiling, stopProfiling } from 'playwright-core/lib/utils'; import { startProfiling, stopProfiling } from 'playwright-core/lib/utils';
import type { TestInfoError } from '../../types/test';
import { serializeError } from '../util'; import { serializeError } from '../util';
import { registerESMLoader } from './esmLoaderHost'; import { registerESMLoader } from './esmLoaderHost';
import { execArgvWithoutExperimentalLoaderOptions } from '../transform/esmUtils'; import { execArgvWithoutExperimentalLoaderOptions } from '../transform/esmUtils';
@ -29,7 +28,7 @@ export type ProtocolRequest = {
export type ProtocolResponse = { export type ProtocolResponse = {
id?: number; id?: number;
error?: TestInfoError; error?: TestInfoErrorImpl;
method?: string; method?: string;
params?: any; params?: any;
result?: any; result?: any;

View file

@ -571,7 +571,7 @@ class ArtifactsRecorder {
if (this._reusedContexts.has(context)) if (this._reusedContexts.has(context))
return; return;
await this._stopTracing(context.tracing); await this._stopTracing(context.tracing);
if (this._screenshotMode === 'on' || this._screenshotMode === 'only-on-failure') { if (this._screenshotMode === 'on' || this._screenshotMode === 'only-on-failure' || (this._screenshotMode === 'on-first-failure' && this._testInfo.retry === 0)) {
// Capture screenshot for now. We'll know whether we have to preserve them // Capture screenshot for now. We'll know whether we have to preserve them
// after the test finishes. // after the test finishes.
await Promise.all(context.pages().map(page => this._screenshotPage(page, true))); await Promise.all(context.pages().map(page => this._screenshotPage(page, true)));
@ -588,14 +588,19 @@ class ArtifactsRecorder {
await this._stopTracing(tracing); await this._stopTracing(tracing);
} }
private _shouldCaptureScreenshotUponFinish() {
return this._screenshotMode === 'on' ||
(this._screenshotMode === 'only-on-failure' && this._testInfo._isFailure()) ||
(this._screenshotMode === 'on-first-failure' && this._testInfo._isFailure() && this._testInfo.retry === 0);
}
async didFinishTestFunction() { async didFinishTestFunction() {
const captureScreenshots = this._screenshotMode === 'on' || (this._screenshotMode === 'only-on-failure' && this._testInfo._isFailure()); if (this._shouldCaptureScreenshotUponFinish())
if (captureScreenshots)
await this._screenshotOnTestFailure(); await this._screenshotOnTestFailure();
} }
async didFinishTest() { async didFinishTest() {
const captureScreenshots = this._screenshotMode === 'on' || (this._screenshotMode === 'only-on-failure' && this._testInfo._isFailure()); const captureScreenshots = this._shouldCaptureScreenshotUponFinish();
if (captureScreenshots) if (captureScreenshots)
await this._screenshotOnTestFailure(); await this._screenshotOnTestFailure();

View file

@ -61,7 +61,7 @@ import {
} from '../common/expectBundle'; } from '../common/expectBundle';
import { zones } from 'playwright-core/lib/utils'; import { zones } from 'playwright-core/lib/utils';
import { TestInfoImpl } from '../worker/testInfo'; import { TestInfoImpl } from '../worker/testInfo';
import { ExpectError, isExpectError } from './matcherHint'; import { ExpectError, isJestError } from './matcherHint';
import { toMatchAriaSnapshot } from './toMatchAriaSnapshot'; import { toMatchAriaSnapshot } from './toMatchAriaSnapshot';
// #region // #region
@ -323,8 +323,13 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
const step = testInfo._addStep(stepInfo); const step = testInfo._addStep(stepInfo);
const reportStepError = (jestError: Error | unknown) => { const reportStepError = (e: Error | unknown) => {
const error = isExpectError(jestError) ? new ExpectError(jestError, customMessage, stackFrames) : jestError; const jestError = isJestError(e) ? e : null;
const error = jestError ? new ExpectError(jestError, customMessage, stackFrames) : e;
if (jestError?.matcherResult.suggestedRebaseline) {
step.complete({ suggestedRebaseline: jestError?.matcherResult.suggestedRebaseline });
return;
}
step.complete({ error }); step.complete({ error });
if (this._info.isSoft) if (this._info.isSoft)
testInfo._failWithError(error); testInfo._failWithError(error);

View file

@ -43,20 +43,21 @@ export type MatcherResult<E, A> = {
printedReceived?: string; printedReceived?: string;
printedExpected?: string; printedExpected?: string;
printedDiff?: string; printedDiff?: string;
suggestedRebaseline?: string;
};
export type MatcherResultProperty = Omit<MatcherResult<unknown, unknown>, 'message'> & {
message: string;
};
type JestError = Error & {
matcherResult: MatcherResultProperty;
}; };
export class ExpectError extends Error { export class ExpectError extends Error {
matcherResult: { matcherResult: MatcherResultProperty;
message: string;
pass: boolean;
name?: string;
expected?: any;
actual?: any;
log?: string[];
timeout?: number;
};
constructor(jestError: ExpectError, customMessage: string, stackFrames: StackFrame[]) { constructor(jestError: JestError, customMessage: string, stackFrames: StackFrame[]) {
super(''); super('');
// Copy to erase the JestMatcherError constructor name from the console.log(error). // Copy to erase the JestMatcherError constructor name from the console.log(error).
this.name = jestError.name; this.name = jestError.name;
@ -69,6 +70,6 @@ export class ExpectError extends Error {
} }
} }
export function isExpectError(e: unknown): e is ExpectError { export function isJestError(e: unknown): e is JestError {
return e instanceof Error && 'matcherResult' in e; return e instanceof Error && 'matcherResult' in e;
} }

View file

@ -22,6 +22,7 @@ import { colors } from 'playwright-core/lib/utilsBundle';
import { EXPECTED_COLOR } from '../common/expectBundle'; import { EXPECTED_COLOR } from '../common/expectBundle';
import { callLogText } from '../util'; import { callLogText } from '../util';
import { printReceivedStringContainExpectedSubstring } from './expect'; import { printReceivedStringContainExpectedSubstring } from './expect';
import { currentTestInfo } from '../common/globals';
export async function toMatchAriaSnapshot( export async function toMatchAriaSnapshot(
this: ExpectMatcherState, this: ExpectMatcherState,
@ -31,6 +32,15 @@ export async function toMatchAriaSnapshot(
): Promise<MatcherResult<string | RegExp, string>> { ): Promise<MatcherResult<string | RegExp, string>> {
const matcherName = 'toMatchAriaSnapshot'; const matcherName = 'toMatchAriaSnapshot';
const testInfo = currentTestInfo();
if (!testInfo)
throw new Error(`toMatchSnapshot() must be called during the test`);
if (testInfo._projectInternal.ignoreSnapshots)
return { pass: !this.isNot, message: () => '', name: 'toMatchSnapshot', expected };
const updateSnapshots = testInfo.config.updateSnapshots;
const matcherOptions = { const matcherOptions = {
isNot: this.isNot, isNot: this.isNot,
promise: this.promise, promise: this.promise,
@ -65,6 +75,12 @@ export async function toMatchAriaSnapshot(
} }
}; };
let suggestedRebaseline: string | undefined;
if (!this.isNot && pass === this.isNot) {
if (updateSnapshots === 'all' || (updateSnapshots === 'missing' && !expected.trim()))
suggestedRebaseline = `toMatchAriaSnapshot(\`\n${unshift(received, '${indent} ')}\n\${indent}\`)`;
}
return { return {
name: matcherName, name: matcherName,
expected, expected,
@ -72,6 +88,7 @@ export async function toMatchAriaSnapshot(
pass, pass,
actual: received, actual: received,
log, log,
suggestedRebaseline,
timeout: timedOut ? timeout : undefined, timeout: timedOut ? timeout : undefined,
}; };
} }
@ -80,7 +97,7 @@ function escapePrivateUsePoints(str: string) {
return str.replace(/[\uE000-\uF8FF]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`); return str.replace(/[\uE000-\uF8FF]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
} }
function unshift(snapshot: string): string { function unshift(snapshot: string, indent: string = ''): string {
const lines = snapshot.split('\n'); const lines = snapshot.split('\n');
let whitespacePrefixLength = 100; let whitespacePrefixLength = 100;
for (const line of lines) { for (const line of lines) {
@ -91,5 +108,5 @@ function unshift(snapshot: string): string {
whitespacePrefixLength = match[1].length; whitespacePrefixLength = match[1].length;
break; break;
} }
return lines.filter(t => t.trim()).map(line => line.substring(whitespacePrefixLength)).join('\n'); return lines.filter(t => t.trim()).map(line => indent + line.substring(whitespacePrefixLength)).join('\n');
} }

View file

@ -27,6 +27,7 @@ import type { FullConfigInternal } from '../common/config';
import type { ReporterV2 } from '../reporters/reporterV2'; import type { ReporterV2 } from '../reporters/reporterV2';
import type { FailureTracker } from './failureTracker'; import type { FailureTracker } from './failureTracker';
import { colors } from 'playwright-core/lib/utilsBundle'; import { colors } from 'playwright-core/lib/utilsBundle';
import { addSuggestedRebaseline } from './rebase';
export type EnvByProjectId = Map<string, Record<string, string | undefined>>; export type EnvByProjectId = Map<string, Record<string, string | undefined>>;
@ -341,6 +342,8 @@ class JobDispatcher {
step.duration = params.wallTime - step.startTime.getTime(); step.duration = params.wallTime - step.startTime.getTime();
if (params.error) if (params.error)
step.error = params.error; step.error = params.error;
if (params.suggestedRebaseline)
addSuggestedRebaseline(step.location!, params.suggestedRebaseline);
steps.delete(params.stepId); steps.delete(params.stepId);
this._reporter.onStepEnd?.(test, result, step); this._reporter.onStepEnd?.(test, result, step);
} }

View file

@ -0,0 +1,95 @@
/**
* 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 path from 'path';
import fs from 'fs';
import type { T } from '../transform/babelBundle';
import { types, traverse, parse } from '../transform/babelBundle';
import { MultiMap } from 'playwright-core/lib/utils';
import { generateUnifiedDiff } from 'playwright-core/lib/utils';
import type { FullConfigInternal } from '../common/config';
import { filterProjects } from './projectUtils';
const t: typeof T = types;
type Location = {
file: string;
line: number;
column: number;
};
type Replacement = {
// Points to the call expression.
location: Location;
code: string;
};
const suggestedRebaselines = new MultiMap<string, Replacement>();
export function addSuggestedRebaseline(location: Location, suggestedRebaseline: string) {
suggestedRebaselines.set(location.file, { location, code: suggestedRebaseline });
}
export async function applySuggestedRebaselines(config: FullConfigInternal) {
if (config.config.updateSnapshots !== 'all' && config.config.updateSnapshots !== 'missing')
return;
const [project] = filterProjects(config.projects, config.cliProjectFilter);
if (!project)
return;
for (const fileName of suggestedRebaselines.keys()) {
const source = await fs.promises.readFile(fileName, 'utf8');
const lines = source.split('\n');
const replacements = suggestedRebaselines.get(fileName);
const fileNode = parse(source, { sourceType: 'module' });
const ranges: { start: number, end: number, oldText: string, newText: string }[] = [];
traverse(fileNode, {
CallExpression: path => {
const node = path.node;
if (node.arguments.length !== 1)
return;
if (!t.isMemberExpression(node.callee))
return;
const argument = node.arguments[0];
if (!t.isStringLiteral(argument) && !t.isTemplateLiteral(argument))
return;
const matcher = node.callee.property;
for (const replacement of replacements) {
// In Babel, rows are 1-based, columns are 0-based.
if (matcher.loc!.start.line !== replacement.location.line)
continue;
if (matcher.loc!.start.column + 1 !== replacement.location.column)
continue;
const indent = lines[matcher.loc!.start.line - 1].match(/^\s*/)![0];
const newText = replacement.code.replace(/\$\{indent\}/g, indent);
ranges.push({ start: matcher.start!, end: node.end!, oldText: source.substring(matcher.start!, node.end!), newText });
}
}
});
ranges.sort((a, b) => b.start - a.start);
let result = source;
for (const range of ranges)
result = result.substring(0, range.start) + range.newText + result.substring(range.end);
const relativeName = path.relative(process.cwd(), fileName);
const patchFile = path.join(project.project.outputDir, 'rebaselines.patch');
await fs.promises.mkdir(path.dirname(patchFile), { recursive: true });
await fs.promises.writeFile(patchFile, generateUnifiedDiff(source, result, relativeName));
}
}

View file

@ -24,6 +24,7 @@ import type { FullConfigInternal } from '../common/config';
import { affectedTestFiles } from '../transform/compilationCache'; import { affectedTestFiles } from '../transform/compilationCache';
import { InternalReporter } from '../reporters/internalReporter'; import { InternalReporter } from '../reporters/internalReporter';
import { LastRunReporter } from './lastRun'; import { LastRunReporter } from './lastRun';
import { applySuggestedRebaselines } from './rebase';
type ProjectConfigWithFiles = { type ProjectConfigWithFiles = {
name: string; name: string;
@ -88,6 +89,8 @@ export class Runner {
]; ];
const status = await runTasks(new TestRun(config, reporter), tasks, config.config.globalTimeout); const status = await runTasks(new TestRun(config, reporter), tasks, config.config.globalTimeout);
await applySuggestedRebaselines(config);
// Calling process.exit() might truncate large stdout/stderr output. // Calling process.exit() might truncate large stdout/stderr output.
// See https://github.com/nodejs/node/issues/6456. // See https://github.com/nodejs/node/issues/6456.
// See https://github.com/nodejs/node/issues/12921 // See https://github.com/nodejs/node/issues/12921

View file

@ -18,6 +18,7 @@ import type { BabelFileResult } from '../../bundles/babel/node_modules/@types/ba
export const codeFrameColumns: typeof import('../../bundles/babel/node_modules/@types/babel__code-frame').codeFrameColumns = require('./babelBundleImpl').codeFrameColumns; export const codeFrameColumns: typeof import('../../bundles/babel/node_modules/@types/babel__code-frame').codeFrameColumns = require('./babelBundleImpl').codeFrameColumns;
export const declare: typeof import('../../bundles/babel/node_modules/@types/babel__helper-plugin-utils').declare = require('./babelBundleImpl').declare; export const declare: typeof import('../../bundles/babel/node_modules/@types/babel__helper-plugin-utils').declare = require('./babelBundleImpl').declare;
export const types: typeof import('../../bundles/babel/node_modules/@types/babel__core').types = require('./babelBundleImpl').types; export const types: typeof import('../../bundles/babel/node_modules/@types/babel__core').types = require('./babelBundleImpl').types;
export const parse: typeof import('../../bundles/babel/node_modules/@babel/parser/typings/babel-parser').parse = require('./babelBundleImpl').parse;
export const traverse: typeof import('../../bundles/babel/node_modules/@types/babel__traverse').default = require('./babelBundleImpl').traverse; export const traverse: typeof import('../../bundles/babel/node_modules/@types/babel__traverse').default = require('./babelBundleImpl').traverse;
export type BabelPlugin = [string, any?]; export type BabelPlugin = [string, any?];
export type BabelTransformFunction = (code: string, filename: string, isTypeScript: boolean, isModule: boolean, pluginsPrefix: BabelPlugin[], pluginsSuffix: BabelPlugin[]) => BabelFileResult; export type BabelTransformFunction = (code: string, filename: string, isTypeScript: boolean, isModule: boolean, pluginsPrefix: BabelPlugin[], pluginsSuffix: BabelPlugin[]) => BabelFileResult;

View file

@ -21,10 +21,10 @@ import path from 'path';
import url from 'url'; import url from 'url';
import { debug, mime, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle'; import { debug, mime, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle';
import { formatCallLog } from 'playwright-core/lib/utils'; import { formatCallLog } from 'playwright-core/lib/utils';
import type { TestInfoError } from './../types/test';
import type { Location } from './../types/testReporter'; import type { Location } from './../types/testReporter';
import { calculateSha1, isRegExp, isString, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils'; import { calculateSha1, isRegExp, isString, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils';
import type { RawStack } from 'playwright-core/lib/utils'; import type { RawStack } from 'playwright-core/lib/utils';
import type { TestInfoErrorImpl } from './common/ipc';
const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..'); const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..');
const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json')); const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json'));
@ -62,7 +62,7 @@ export function filteredStackTrace(rawStack: RawStack): StackFrame[] {
return frames; return frames;
} }
export function serializeError(error: Error | any): TestInfoError { export function serializeError(error: Error | any): TestInfoErrorImpl {
if (error instanceof Error) if (error instanceof Error)
return filterStackTrace(error); return filterStackTrace(error);
return { return {

View file

@ -3,3 +3,4 @@
../transform/ ../transform/
../util.ts ../util.ts
../utilBundle.ts ../utilBundle.ts
../matchers/**

View file

@ -17,8 +17,8 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { captureRawStack, monotonicTime, zones, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils'; import { captureRawStack, monotonicTime, zones, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils';
import type { TestInfoError, TestInfo, TestStatus, FullProject } from '../../types/test'; import type { TestInfo, TestStatus, FullProject } from '../../types/test';
import type { AttachmentPayload, StepBeginPayload, StepEndPayload, WorkerInitParams } from '../common/ipc'; import type { AttachmentPayload, StepBeginPayload, StepEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc';
import type { TestCase } from '../common/test'; import type { TestCase } from '../common/test';
import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager'; import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
import type { RunnableDescription } from './timeoutManager'; import type { RunnableDescription } from './timeoutManager';
@ -28,10 +28,10 @@ import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normal
import { TestTracing } from './testTracing'; import { TestTracing } from './testTracing';
import type { Attachment } from './testTracing'; import type { Attachment } from './testTracing';
import type { StackFrame } from '@protocol/channels'; import type { StackFrame } from '@protocol/channels';
import { serializeWorkerError } from './util'; import { testInfoError } from './util';
export interface TestStepInternal { export interface TestStepInternal {
complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void; complete(result: { error?: Error | unknown, attachments?: Attachment[], suggestedRebaseline?: string }): void;
stepId: string; stepId: string;
title: string; title: string;
category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'attach' | string; category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'attach' | string;
@ -41,7 +41,7 @@ export interface TestStepInternal {
endWallTime?: number; endWallTime?: number;
apiName?: string; apiName?: string;
params?: Record<string, any>; params?: Record<string, any>;
error?: TestInfoError; error?: TestInfoErrorImpl;
infectParentStepsWithError?: boolean; infectParentStepsWithError?: boolean;
box?: boolean; box?: boolean;
isStage?: boolean; isStage?: boolean;
@ -97,14 +97,14 @@ export class TestInfoImpl implements TestInfo {
snapshotSuffix: string = ''; snapshotSuffix: string = '';
readonly outputDir: string; readonly outputDir: string;
readonly snapshotDir: string; readonly snapshotDir: string;
errors: TestInfoError[] = []; errors: TestInfoErrorImpl[] = [];
readonly _attachmentsPush: (...items: TestInfo['attachments']) => number; readonly _attachmentsPush: (...items: TestInfo['attachments']) => number;
get error(): TestInfoError | undefined { get error(): TestInfoErrorImpl | undefined {
return this.errors[0]; return this.errors[0];
} }
set error(e: TestInfoError | undefined) { set error(e: TestInfoErrorImpl | undefined) {
if (e === undefined) if (e === undefined)
throw new Error('Cannot assign testInfo.error undefined value!'); throw new Error('Cannot assign testInfo.error undefined value!');
this.errors[0] = e; this.errors[0] = e;
@ -273,7 +273,7 @@ export class TestInfoImpl implements TestInfo {
if (result.error) { if (result.error) {
if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol]) if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol])
(result.error as any)[stepSymbol] = step; (result.error as any)[stepSymbol] = step;
const error = serializeWorkerError(result.error); const error = testInfoError(result.error);
if (data.boxedStack) if (data.boxedStack)
error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`; error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`;
step.error = error; step.error = error;
@ -297,6 +297,7 @@ export class TestInfoImpl implements TestInfo {
stepId, stepId,
wallTime: step.endWallTime, wallTime: step.endWallTime,
error: step.error, error: step.error,
suggestedRebaseline: result.suggestedRebaseline,
}; };
this._onStepEnd(payload); this._onStepEnd(payload);
const errorForTrace = step.error ? { name: '', message: step.error.message || '', stack: step.error.stack } : undefined; const errorForTrace = step.error ? { name: '', message: step.error.message || '', stack: step.error.stack } : undefined;
@ -331,7 +332,7 @@ export class TestInfoImpl implements TestInfo {
_failWithError(error: Error | unknown) { _failWithError(error: Error | unknown) {
if (this.status === 'passed' || this.status === 'skipped') if (this.status === 'passed' || this.status === 'skipped')
this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed'; this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed';
const serialized = serializeWorkerError(error); const serialized = testInfoError(error);
const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined; const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined;
if (step && step.boxedStack) if (step && step.boxedStack)
serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`; serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;

View file

@ -21,10 +21,10 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { ManualPromise, calculateSha1, monotonicTime, createGuid, SerializedFS } from 'playwright-core/lib/utils'; import { ManualPromise, calculateSha1, monotonicTime, createGuid, SerializedFS } from 'playwright-core/lib/utils';
import { yauzl, yazl } from 'playwright-core/lib/zipBundle'; import { yauzl, yazl } from 'playwright-core/lib/zipBundle';
import type { TestInfo, TestInfoError } from '../../types/test';
import { filteredStackTrace } from '../util'; import { filteredStackTrace } from '../util';
import type { TraceMode, PlaywrightWorkerOptions } from '../../types/test'; import type { TestInfo, TraceMode, PlaywrightWorkerOptions } from '../../types/test';
import type { TestInfoImpl } from './testInfo'; import type { TestInfoImpl } from './testInfo';
import type { TestInfoErrorImpl } from '../common/ipc';
export type Attachment = TestInfo['attachments'][0]; export type Attachment = TestInfo['attachments'][0];
export const testTraceEntryName = 'test.trace'; export const testTraceEntryName = 'test.trace';
@ -219,7 +219,7 @@ export class TestTracing {
this._testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' }); this._testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
} }
appendForError(error: TestInfoError) { appendForError(error: TestInfoErrorImpl) {
const rawStack = error.stack?.split('\n') || []; const rawStack = error.stack?.split('\n') || [];
const stack = rawStack ? filteredStackTrace(rawStack) : []; const stack = rawStack ? filteredStackTrace(rawStack) : [];
this._appendTraceEvent({ this._appendTraceEvent({

View file

@ -14,32 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
import type { TestError } from '../../types/testReporter'; import type { TestInfoErrorImpl } from '../common/ipc';
import type { TestInfoError } from '../../types/test'; import { ExpectError } from '../matchers/matcherHint';
import type { MatcherResult } from '../matchers/matcherHint';
import { serializeError } from '../util'; import { serializeError } from '../util';
export function testInfoError(error: Error | any): TestInfoErrorImpl {
type MatcherResultDetails = Pick<TestError, 'timeout'|'matcherName'|'locator'|'expected'|'received'|'log'>; const result = serializeError(error);
if (error instanceof ExpectError)
export function serializeWorkerError(error: Error | any): TestInfoError & MatcherResultDetails { result.matcherResult = error.matcherResult;
return { return result;
...serializeError(error),
...serializeExpectDetails(error),
};
} }
function serializeExpectDetails(e: Error): MatcherResultDetails {
const matcherResult = (e as any).matcherResult as MatcherResult<unknown, unknown>;
if (!matcherResult)
return {};
return {
timeout: matcherResult.timeout,
matcherName: matcherResult.name,
locator: matcherResult.locator,
expected: matcherResult.printedExpected,
received: matcherResult.printedReceived,
log: matcherResult.log,
};
}

View file

@ -16,7 +16,8 @@
import { colors } from 'playwright-core/lib/utilsBundle'; import { colors } from 'playwright-core/lib/utilsBundle';
import { debugTest, relativeFilePath } from '../util'; import { debugTest, relativeFilePath } from '../util';
import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc'; import type { TestBeginPayload, TestEndPayload, RunPayload, DonePayload, WorkerInitParams, TeardownErrorsPayload, TestInfoErrorImpl } from '../common/ipc';
import { stdioChunkToParams } from '../common/ipc';
import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals'; import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals';
import { deserializeConfig } from '../common/configLoader'; import { deserializeConfig } from '../common/configLoader';
import type { Suite, TestCase } from '../common/test'; import type { Suite, TestCase } from '../common/test';
@ -28,11 +29,10 @@ import { ProcessRunner } from '../common/process';
import { loadTestFile } from '../common/testLoader'; import { loadTestFile } from '../common/testLoader';
import { applyRepeatEachIndex, bindFileSuiteToProject, filterTestsRemoveEmptySuites } from '../common/suiteUtils'; import { applyRepeatEachIndex, bindFileSuiteToProject, filterTestsRemoveEmptySuites } from '../common/suiteUtils';
import { PoolBuilder } from '../common/poolBuilder'; import { PoolBuilder } from '../common/poolBuilder';
import type { TestInfoError } from '../../types/test';
import type { Location } from '../../types/testReporter'; import type { Location } from '../../types/testReporter';
import { inheritFixtureNames } from '../common/fixtures'; import { inheritFixtureNames } from '../common/fixtures';
import { type TimeSlot } from './timeoutManager'; import { type TimeSlot } from './timeoutManager';
import { serializeWorkerError } from './util'; import { testInfoError } from './util';
export class WorkerMain extends ProcessRunner { export class WorkerMain extends ProcessRunner {
private _params: WorkerInitParams; private _params: WorkerInitParams;
@ -42,7 +42,7 @@ export class WorkerMain extends ProcessRunner {
private _fixtureRunner: FixtureRunner; private _fixtureRunner: FixtureRunner;
// Accumulated fatal errors that cannot be attributed to a test. // Accumulated fatal errors that cannot be attributed to a test.
private _fatalErrors: TestInfoError[] = []; private _fatalErrors: TestInfoErrorImpl[] = [];
// Whether we should skip running remaining tests in this suite because // Whether we should skip running remaining tests in this suite because
// of a setup error, usually beforeAll hook. // of a setup error, usually beforeAll hook.
private _skipRemainingTestsInSuite: Suite | undefined; private _skipRemainingTestsInSuite: Suite | undefined;
@ -113,7 +113,7 @@ export class WorkerMain extends ProcessRunner {
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {}); await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {});
this._fatalErrors.push(...fakeTestInfo.errors); this._fatalErrors.push(...fakeTestInfo.errors);
} catch (e) { } catch (e) {
this._fatalErrors.push(serializeWorkerError(e)); this._fatalErrors.push(testInfoError(e));
} }
if (this._fatalErrors.length) { if (this._fatalErrors.length) {
@ -123,7 +123,7 @@ export class WorkerMain extends ProcessRunner {
} }
} }
private _appendProcessTeardownDiagnostics(error: TestInfoError) { private _appendProcessTeardownDiagnostics(error: TestInfoErrorImpl) {
if (!this._lastRunningTests.length) if (!this._lastRunningTests.length)
return; return;
const count = this._totalRunningTests === 1 ? '1 test' : `${this._totalRunningTests} tests`; const count = this._totalRunningTests === 1 ? '1 test' : `${this._totalRunningTests} tests`;
@ -154,7 +154,7 @@ export class WorkerMain extends ProcessRunner {
// No current test - fatal error. // No current test - fatal error.
if (!this._currentTest) { if (!this._currentTest) {
if (!this._fatalErrors.length) if (!this._fatalErrors.length)
this._fatalErrors.push(serializeWorkerError(error)); this._fatalErrors.push(testInfoError(error));
void this._stop(); void this._stop();
return; return;
} }
@ -225,7 +225,7 @@ export class WorkerMain extends ProcessRunner {
// In theory, we should run above code without any errors. // In theory, we should run above code without any errors.
// However, in the case we screwed up, or loadTestFile failed in the worker // However, in the case we screwed up, or loadTestFile failed in the worker
// but not in the runner, let's do a fatal error. // but not in the runner, let's do a fatal error.
this._fatalErrors.push(serializeWorkerError(e)); this._fatalErrors.push(testInfoError(e));
void this._stop(); void this._stop();
} finally { } finally {
const donePayload: DonePayload = { const donePayload: DonePayload = {

View file

@ -5863,6 +5863,7 @@ export interface PlaywrightWorkerOptions {
* - `'off'`: Do not capture screenshots. * - `'off'`: Do not capture screenshots.
* - `'on'`: Capture screenshot after each test. * - `'on'`: Capture screenshot after each test.
* - `'only-on-failure'`: Capture screenshot after each test failure. * - `'only-on-failure'`: Capture screenshot after each test failure.
* - `'on-first-failure'`: Capture screenshot after each test's first failure.
* *
* **Usage** * **Usage**
* *
@ -5938,7 +5939,7 @@ export interface PlaywrightWorkerOptions {
video: VideoMode | /** deprecated */ 'retry-with-video' | { mode: VideoMode, size?: ViewportSize }; video: VideoMode | /** deprecated */ 'retry-with-video' | { mode: VideoMode, size?: ViewportSize };
} }
export type ScreenshotMode = 'off' | 'on' | 'only-on-failure'; export type ScreenshotMode = 'off' | 'on' | 'only-on-failure' | 'on-first-failure';
export type TraceMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'on-all-retries' | 'retain-on-first-failure'; export type TraceMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'on-all-retries' | 'retain-on-first-failure';
export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry'; export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry';

View file

@ -1777,6 +1777,7 @@ export type BrowserContextEnableRecorderParams = {
device?: string, device?: string,
saveStorage?: string, saveStorage?: string,
outputFile?: string, outputFile?: string,
handleSIGINT?: boolean,
omitCallTracking?: boolean, omitCallTracking?: boolean,
}; };
export type BrowserContextEnableRecorderOptions = { export type BrowserContextEnableRecorderOptions = {
@ -1790,6 +1791,7 @@ export type BrowserContextEnableRecorderOptions = {
device?: string, device?: string,
saveStorage?: string, saveStorage?: string,
outputFile?: string, outputFile?: string,
handleSIGINT?: boolean,
omitCallTracking?: boolean, omitCallTracking?: boolean,
}; };
export type BrowserContextEnableRecorderResult = void; export type BrowserContextEnableRecorderResult = void;

View file

@ -1208,6 +1208,7 @@ BrowserContext:
device: string? device: string?
saveStorage: string? saveStorage: string?
outputFile: string? outputFile: string?
handleSIGINT: boolean?
omitCallTracking: boolean? omitCallTracking: boolean?
newCDPSession: newCDPSession:

View file

@ -36,6 +36,7 @@ export type BrowserTestWorkerFixtures = PageWorkerFixtures & {
browserType: BrowserType; browserType: BrowserType;
isAndroid: boolean; isAndroid: boolean;
isElectron: boolean; isElectron: boolean;
nodeVersion: { major: number, minor: number, patch: number };
bidiTestSkipPredicate: (info: TestInfo) => boolean; bidiTestSkipPredicate: (info: TestInfo) => boolean;
}; };
@ -96,6 +97,11 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
await run(Number(browserVersion.split('.')[0])); await run(Number(browserVersion.split('.')[0]));
}, { scope: 'worker' }], }, { scope: 'worker' }],
nodeVersion: [async ({}, use) => {
const [major, minor, patch] = process.versions.node.split('.');
await use({ major: +major, minor: +minor, patch: +patch });
}, { scope: 'worker' }],
isAndroid: [false, { scope: 'worker' }], isAndroid: [false, { scope: 'worker' }],
isElectron: [false, { scope: 'worker' }], isElectron: [false, { scope: 'worker' }],
electronMajorVersion: [0, { scope: 'worker' }], electronMajorVersion: [0, { scope: 'worker' }],

View file

@ -880,9 +880,9 @@ it('should respect timeout after redirects', async function({ context, server })
expect(error.message).toContain(`Request timed out after 100ms`); expect(error.message).toContain(`Request timed out after 100ms`);
}); });
it('should not hang on a brotli encoded Range request', async ({ context, server }) => { it('should not hang on a brotli encoded Range request', async ({ context, server, nodeVersion }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/18190' }); it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/18190' });
it.skip(+process.versions.node.split('.')[0] < 18); it.skip(nodeVersion.major < 18);
const encodedRequestPayload = zlib.brotliCompressSync(Buffer.from('A')); const encodedRequestPayload = zlib.brotliCompressSync(Buffer.from('A'));
server.setRoute('/brotli', (req, res) => { server.setRoute('/brotli', (req, res) => {
@ -1094,10 +1094,9 @@ it('should support multipart/form-data and keep the order', async function({ con
expect(response.status()).toBe(200); expect(response.status()).toBe(200);
}); });
it('should support repeating names in multipart/form-data', async function({ context, server }) { it('should support repeating names in multipart/form-data', async function({ context, server, nodeVersion }) {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28070' }); it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/28070' });
const nodeVersion = +process.versions.node.split('.')[0]; it.skip(nodeVersion.major < 20, 'File is not available in Node.js < 20. FormData is not available in Node.js < 18');
it.skip(nodeVersion < 20, 'File is not available in Node.js < 20. FormData is not available in Node.js < 18');
const postBodyPromise = new Promise<string>(resolve => { const postBodyPromise = new Promise<string>(resolve => {
server.setRoute('/empty.html', async (req, res) => { server.setRoute('/empty.html', async (req, res) => {
resolve((await req.postBody).toString('utf-8')); resolve((await req.postBody).toString('utf-8'));

View file

@ -204,13 +204,13 @@ it('should handle missing file', async ({ contextFactory }, testInfo) => {
expect(error.message).toContain(`Error reading storage state from ${file}:\nENOENT`); expect(error.message).toContain(`Error reading storage state from ${file}:\nENOENT`);
}); });
it('should handle malformed file', async ({ contextFactory }, testInfo) => { it('should handle malformed file', async ({ contextFactory, nodeVersion }, testInfo) => {
const file = testInfo.outputPath('state.json'); const file = testInfo.outputPath('state.json');
fs.writeFileSync(file, 'not-json', 'utf-8'); fs.writeFileSync(file, 'not-json', 'utf-8');
const error = await contextFactory({ const error = await contextFactory({
storageState: file, storageState: file,
}).catch(e => e); }).catch(e => e);
if (+process.versions.node.split('.')[0] > 18) if (nodeVersion.major > 18)
expect(error.message).toContain(`Error reading storage state from ${file}:\nUnexpected token 'o', \"not-json\" is not valid JSON`); expect(error.message).toContain(`Error reading storage state from ${file}:\nUnexpected token 'o', \"not-json\" is not valid JSON`);
else else
expect(error.message).toContain(`Error reading storage state from ${file}:\nUnexpected token o in JSON at position 1`); expect(error.message).toContain(`Error reading storage state from ${file}:\nUnexpected token o in JSON at position 1`);

View file

@ -130,9 +130,8 @@ it('should be callable twice', async ({ browserType }) => {
await browser.close(); await browser.close();
}); });
it('should allow await using', async ({ browserType }) => { it('should allow await using', async ({ browserType, nodeVersion }) => {
const nodeVersion = +process.versions.node.split('.')[0]; it.skip(nodeVersion.major < 18);
it.skip(nodeVersion < 18);
let b: Browser; let b: Browser;
let c: BrowserContext; let c: BrowserContext;

View file

@ -401,6 +401,7 @@ it('should be able to render avif images', {
}, async ({ page, server, browserName, platform }) => { }, async ({ page, server, browserName, platform }) => {
it.fixme(browserName === 'webkit' && platform === 'win32'); it.fixme(browserName === 'webkit' && platform === 'win32');
it.skip(browserName === 'webkit' && hostPlatform.startsWith('ubuntu20.04'), 'Ubuntu 20.04 is frozen'); it.skip(browserName === 'webkit' && hostPlatform.startsWith('ubuntu20.04'), 'Ubuntu 20.04 is frozen');
it.skip(browserName === 'webkit' && hostPlatform.startsWith('debian11'), 'Debian 11 is too old');
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.setContent(`<img src="${server.PREFIX}/rgb.avif" onerror="window.error = true">`); await page.setContent(`<img src="${server.PREFIX}/rgb.avif" onerror="window.error = true">`);
await expect.poll(() => page.locator('img').boundingBox()).toEqual(expect.objectContaining({ await expect.poll(() => page.locator('img').boundingBox()).toEqual(expect.objectContaining({

View file

@ -52,7 +52,8 @@ it('should open devtools when "devtools: true" option is given', async ({ browse
await browser.close(); await browser.close();
}); });
it('should return background pages', async ({ browserType, createUserDataDir, asset }) => { it('should return background pages', async ({ browserType, createUserDataDir, asset, channel }) => {
it.skip(channel === 'chromium-headless-shell', 'Headless Shell has no support for extensions');
const userDataDir = await createUserDataDir(); const userDataDir = await createUserDataDir();
const extensionPath = asset('simple-extension'); const extensionPath = asset('simple-extension');
const extensionOptions = { const extensionOptions = {
@ -75,7 +76,8 @@ it('should return background pages', async ({ browserType, createUserDataDir, as
expect(context.backgroundPages().length).toBe(0); expect(context.backgroundPages().length).toBe(0);
}); });
it('should return background pages when recording video', async ({ browserType, createUserDataDir, asset }, testInfo) => { it('should return background pages when recording video', async ({ browserType, createUserDataDir, asset, channel }, testInfo) => {
it.skip(channel === 'chromium-headless-shell', 'Headless Shell has no support for extensions');
const userDataDir = await createUserDataDir(); const userDataDir = await createUserDataDir();
const extensionPath = asset('simple-extension'); const extensionPath = asset('simple-extension');
const extensionOptions = { const extensionOptions = {
@ -99,7 +101,8 @@ it('should return background pages when recording video', async ({ browserType,
await context.close(); await context.close();
}); });
it('should support request/response events when using backgroundPage()', async ({ browserType, createUserDataDir, asset, server }) => { it('should support request/response events when using backgroundPage()', async ({ browserType, createUserDataDir, asset, server, channel }) => {
it.skip(channel === 'chromium-headless-shell', 'Headless Shell has no support for extensions');
server.setRoute('/empty.html', (req, res) => { server.setRoute('/empty.html', (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html', 'x-response-foobar': 'BarFoo' }); res.writeHead(200, { 'Content-Type': 'text/html', 'x-response-foobar': 'BarFoo' });
res.end(`<span>hello world!</span>`); res.end(`<span>hello world!</span>`);
@ -148,7 +151,8 @@ it('should support request/response events when using backgroundPage()', async (
it('should report console messages from content script', { it('should report console messages from content script', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32762' } annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32762' }
}, async ({ browserType, createUserDataDir, asset, server }) => { }, async ({ browserType, createUserDataDir, asset, server, channel }) => {
it.skip(channel === 'chromium-headless-shell', 'Headless Shell has no support for extensions');
const userDataDir = await createUserDataDir(); const userDataDir = await createUserDataDir();
const extensionPath = asset('extension-with-logging'); const extensionPath = asset('extension-with-logging');
const extensionOptions = { const extensionOptions = {

View file

@ -727,9 +727,9 @@ test.describe('browser', () => {
await browser.close(); await browser.close();
}); });
test('should return target connection errors when using http2', async ({ browser, startCCServer, asset, browserName, isMac, isLinux }) => { test('should return target connection errors when using http2', async ({ browser, startCCServer, asset, browserName, isMac, nodeVersion }) => {
test.skip(browserName === 'webkit' && isMac, 'WebKit on macOS does not proxy localhost'); test.skip(browserName === 'webkit' && isMac, 'WebKit on macOS does not proxy localhost');
test.skip(+process.versions.node.split('.')[0] < 20, 'http2.performServerHandshake is not supported in older Node.js versions'); test.skip(nodeVersion.major < 20, 'http2.performServerHandshake is not supported in older Node.js versions');
const serverURL = await startCCServer({ http2: true }); const serverURL = await startCCServer({ http2: true });
const page = await browser.newPage({ const page = await browser.newPage({

View file

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

View file

@ -41,9 +41,10 @@ it('should kill browser process on timeout after close', async ({ browserType, m
expect(stalled).toBeTruthy(); expect(stalled).toBeTruthy();
}); });
it('should throw a friendly error if its headed and there is no xserver on linux running', async ({ mode, browserType, platform }) => { it('should throw a friendly error if its headed and there is no xserver on linux running', async ({ mode, browserType, platform, channel }) => {
it.skip(platform !== 'linux'); it.skip(platform !== 'linux');
it.skip(mode.startsWith('service')); it.skip(mode.startsWith('service'));
it.skip(channel === 'chromium-headless-shell', 'Headless Shell is always headless');
const error: Error = await browserType.launch({ const error: Error = await browserType.launch({
headless: false, headless: false,

View file

@ -584,3 +584,22 @@ it('parse locators strictly', () => {
expect.soft(parseLocator('javascript', `locator('div').filter({ hasText: 'Goodbye world' }}).locator('span')`)).not.toBe(selector); expect.soft(parseLocator('javascript', `locator('div').filter({ hasText: 'Goodbye world' }}).locator('span')`)).not.toBe(selector);
expect.soft(parseLocator('python', `locator("div").filter(has_text=="Goodbye world").locator("span")`)).not.toBe(selector); expect.soft(parseLocator('python', `locator("div").filter(has_text=="Goodbye world").locator("span")`)).not.toBe(selector);
}); });
it('parseLocator frames', async () => {
expect.soft(parseLocator('javascript', `locator('iframe').contentFrame().getByText('foo')`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('javascript', `frameLocator('iframe').getByText('foo')`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('javascript', `frameLocator('css=iframe').getByText('foo')`, '')).toBe(`css=iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('javascript', `getByTitle('iframe title').contentFrame()`)).toBe(`internal:attr=[title=\"iframe title\"i] >> internal:control=enter-frame`);
expect.soft(asLocators('javascript', 'internal:attr=[title=\"iframe title\"i] >> internal:control=enter-frame')).toEqual([`getByTitle('iframe title').contentFrame()`]);
expect.soft(parseLocator('python', `locator("iframe").content_frame.get_by_text("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('python', `frame_locator("iframe").get_by_text("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('python', `frame_locator("css=iframe").get_by_text("foo")`, '')).toBe(`css=iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('csharp', `Locator("iframe").ContentFrame.GetByText("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('csharp', `FrameLocator("iframe").GetByText("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('java', `locator("iframe").contentFrame().getByText("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
expect.soft(parseLocator('java', `frameLocator("iframe").getByText("foo")`, '')).toBe(`iframe >> internal:control=enter-frame >> internal:text=\"foo\"i`);
});

View file

@ -1415,26 +1415,37 @@ test('should not leak recorders', {
}, async ({ showTraceViewer }) => { }, async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]); const traceViewer = await showTraceViewer([traceFile]);
const counts = async () => { const aliveCount = async () => {
return await traceViewer.page.evaluate(() => { return await traceViewer.page.evaluate(() => {
const weakSet = (window as any)._weakRecordersForTest || new Set(); const weakSet = (window as any)._weakRecordersForTest || new Set();
const weakList = [...weakSet]; const weakList = [...weakSet];
const aliveList = weakList.filter(r => !!r.deref()); const aliveList = weakList.filter(r => !!r.deref());
return { total: weakList.length, alive: aliveList.length }; return aliveList.length;
}); });
}; };
await traceViewer.snapshotFrame('page.goto'); await expect(traceViewer.snapshotContainer.contentFrame().locator('body')).toContainText(`Hi, I'm frame`);
await traceViewer.snapshotFrame('page.evaluate');
await traceViewer.page.requestGC(); const frame1 = await traceViewer.snapshotFrame('page.goto');
await expect.poll(() => counts()).toEqual({ total: 4, alive: 1 }); await expect(frame1.locator('body')).toContainText('Hello world');
const frame2 = await traceViewer.snapshotFrame('page.evaluate');
await expect(frame2.locator('button')).toBeVisible();
await traceViewer.snapshotFrame('page.setContent');
await traceViewer.snapshotFrame('page.goto');
await traceViewer.snapshotFrame('page.evaluate');
await traceViewer.snapshotFrame('page.setContent');
await traceViewer.page.requestGC(); await traceViewer.page.requestGC();
await expect.poll(() => counts()).toEqual({ total: 8, alive: 1 }); await expect.poll(() => aliveCount()).toBeLessThanOrEqual(2); // two snapshot iframes
const frame3 = await traceViewer.snapshotFrame('page.setViewportSize');
await expect(frame3.locator('body')).toContainText(`Hi, I'm frame`);
const frame4 = await traceViewer.snapshotFrame('page.goto');
await expect(frame4.locator('body')).toContainText('Hello world');
const frame5 = await traceViewer.snapshotFrame('page.evaluate');
await expect(frame5.locator('button')).toBeVisible();
await traceViewer.page.requestGC();
await expect.poll(() => aliveCount()).toBeLessThanOrEqual(2); // two snapshot iframes
}); });
test('should serve css without content-type', async ({ page, runAndTrace, server }) => { test('should serve css without content-type', async ({ page, runAndTrace, server }) => {

View file

@ -15,8 +15,8 @@
*/ */
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { createClock as rawCreateClock, install as rawInstall } from '../../packages/playwright-core/src/server/injected/clock'; import { createClock as rawCreateClock, install as rawInstall } from '../../../packages/playwright-core/src/server/injected/clock';
import type { InstallConfig, ClockController, ClockMethods } from '../../packages/playwright-core/src/server/injected/clock'; import type { InstallConfig, ClockController, ClockMethods } from '../../../packages/playwright-core/src/server/injected/clock';
const createClock = (now?: number): ClockController & ClockMethods => { const createClock = (now?: number): ClockController & ClockMethods => {
const { clock, api } = rawCreateClock(globalThis); const { clock, api } = rawCreateClock(globalThis);

View file

@ -0,0 +1,260 @@
/**
* 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 as it, expect } from '@playwright/test';
import { generateUnifiedDiff } from '../../../packages/playwright-core/lib/utils/patch';
it('Identical texts should produce an empty diff', () => {
const text1 = `line1
line2
line3`;
const text2 = `line1
line2
line3`;
const diff = generateUnifiedDiff(text1, text2);
expect(diff).toBe(`--- a/file
+++ b/file
`);
});
it('Text with an inserted line', () => {
const text1 = `line1
line2
line3`;
const text2 = `line1
line2
line2.5
line3`;
const expectedDiff = `--- a/file
+++ b/file
@@ -1,3 +1,4 @@
line1
line2
+line2.5
line3
`;
const diff = generateUnifiedDiff(text1, text2);
expect(diff).toContain(expectedDiff);
});
it('Text with a deleted line', () => {
const text1 = `line1
line2
line3`;
const text2 = `line1
line3`;
const expectedDiff = `--- a/file
+++ b/file
@@ -1,3 +1,2 @@
line1
-line2
line3
`;
const diff = generateUnifiedDiff(text1, text2);
expect(diff).toContain(expectedDiff);
});
it('Text with modified line', () => {
const text1 = `line1
line2
line3`;
const text2 = `line1
line2 modified
line3`;
const expectedDiff = `--- a/file
+++ b/file
@@ -1,3 +1,3 @@
line1
-line2
+line2 modified
line3
`;
const diff = generateUnifiedDiff(text1, text2);
expect(diff).toContain(expectedDiff);
});
it('Empty original text', () => {
const text1 = ``;
const text2 = `line1
line2`;
const expectedDiff = `--- a/file
+++ b/file
@@ -0,0 +1,2 @@
+line1
+line2
`;
const diff = generateUnifiedDiff(text1, text2);
expect(diff).toContain(expectedDiff);
});
it('Empty modified text', () => {
const text1 = `line1
line2`;
const text2 = ``;
const expectedDiff = `--- a/file
+++ b/file
@@ -1,2 +0,0 @@
-line1
-line2
`;
const diff = generateUnifiedDiff(text1, text2);
expect(diff).toContain(expectedDiff);
});
it('Handling different line endings (CRLF vs LF)', () => {
const text1 = `line1\r\nline2\r\nline3`;
const text2 = `line1\nline2 modified\nline3`;
const expectedDiff = `--- a/file
+++ b/file
@@ -1,3 +1,3 @@
line1
-line2
+line2 modified
line3
`;
const diff = generateUnifiedDiff(text1, text2);
expect(diff).toContain(expectedDiff);
});
it('Large text diff', () => {
const text1 = Array(1000)
.fill('line')
.join('\n');
const text2 = Array(1000)
.fill('line')
.map((line, index) => (index === 500 ? 'modified line' : line))
.join('\n');
const diff = generateUnifiedDiff(text1, text2);
expect(diff).toContain('-line\n+modified line');
});
it('Unicode characters', () => {
const text1 = `こんにちは
`;
const text2 = `こんにちは
`;
const expectedDiff = `--- a/file
+++ b/file
@@ -1,2 +1,3 @@
-
+
+
`;
const diff = generateUnifiedDiff(text1, text2);
expect(diff).toContain(expectedDiff);
});
it('Texts with only whitespace differences', () => {
const text1 = `line1
line2
line3`;
const text2 = `line1
line2
line3`;
const expectedDiff = `--- a/file
+++ b/file
@@ -1,3 +1,3 @@
line1
-line2
+line2
line3
`;
const diff = generateUnifiedDiff(text1, text2);
expect(diff).toBe(expectedDiff);
});
it('Custom file names in diff header', () => {
const text1 = `line1
line2
line3`;
const text2 = `line1
line2 modified
line3`;
const diff = generateUnifiedDiff(text1, text2, 'original.txt');
expect(diff.startsWith('--- a/original.txt\n+++ b/original.txt\n')).toBe(true);
});
it('Multiple consecutive insertions and deletions', () => {
const text1 = `line1
line2
line3
line4
line5`;
const text2 = `line1
line2 modified
line3
line4 modified
line5`;
const diff = generateUnifiedDiff(text1, text2);
expect(diff).toContain('-line2\n+line2 modified');
expect(diff).toContain('-line4\n+line4 modified');
});
it('Handling tabs and special characters', () => {
const text1 = `line1
line\t2
line3`;
const text2 = `line1
line2
line3`;
const expectedDiff = `--- a/file
+++ b/file
@@ -1,3 +1,3 @@
line1
-line\t2
+line2
line3
`;
const diff = generateUnifiedDiff(text1, text2);
expect(diff).toContain(expectedDiff);
});
it('Texts with leading and trailing whitespace differences', () => {
const text1 = ` line1
line2
line3`;
const text2 = `line1
line2
line3`;
const expectedDiff = `--- a/file
+++ b/file
@@ -1,3 +1,3 @@
- line1
-line2
+line1
+line2
line3
`;
const diff = generateUnifiedDiff(text1, text2);
expect(diff).toContain(expectedDiff);
});

View file

@ -16,7 +16,7 @@
import { test as it, expect } from '@playwright/test'; import { test as it, expect } from '@playwright/test';
import { findRepeatedSubsequences } from '../../packages/playwright-core/lib/utils/sequence'; import { findRepeatedSubsequences } from '../../../packages/playwright-core/lib/utils/sequence';
it('should return an empty array when the input is empty', () => { it('should return an empty array when the input is empty', () => {
const input = []; const input = [];

View file

@ -385,3 +385,17 @@ it('should include pseudo codepoints', async ({ page, server }) => {
- paragraph: "\ueab2hello" - paragraph: "\ueab2hello"
`); `);
}); });
it('check aria-hidden text', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
await page.setContent(`
<p>
<span>hello</span>
<span aria-hidden="true">world</span>
</p>
`);
await checkAndMatchSnapshot(page.locator('body'), `
- paragraph: "hello"
`);
});

View file

@ -286,7 +286,7 @@ it.describe('page screenshot', () => {
await page.goto(server.PREFIX + '/screenshots/canvas.html'); await page.goto(server.PREFIX + '/screenshots/canvas.html');
const screenshot = await page.screenshot(); const screenshot = await page.screenshot();
if ((!headless && browserName === 'chromium' && isMac && os.arch() === 'arm64' && macVersion >= 14) || if ((!headless && browserName === 'chromium' && isMac && os.arch() === 'arm64' && macVersion >= 14) ||
(browserName === 'webkit' && isLinux)) (browserName === 'webkit' && isLinux && os.arch() === 'x64'))
expect(screenshot).toMatchSnapshot('screenshot-canvas-with-accurate-corners.png'); expect(screenshot).toMatchSnapshot('screenshot-canvas-with-accurate-corners.png');
else else
expect(screenshot).toMatchSnapshot('screenshot-canvas.png'); expect(screenshot).toMatchSnapshot('screenshot-canvas.png');

View file

@ -43,8 +43,8 @@ test('should match list with accessible name', async ({ page }) => {
`); `);
await expect(page.locator('body')).toMatchAriaSnapshot(` await expect(page.locator('body')).toMatchAriaSnapshot(`
- list "my list": - list "my list":
- listitem: one - listitem: "one"
- listitem: two - listitem: "two"
`); `);
}); });
@ -90,7 +90,7 @@ test('should allow text nodes', async ({ page }) => {
await expect(page.locator('body')).toMatchAriaSnapshot(` await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "Microsoft" - heading "Microsoft"
- text: Open source projects and samples from Microsoft - text: "Open source projects and samples from Microsoft"
`); `);
}); });
@ -103,7 +103,7 @@ test('details visibility', async ({ page }) => {
`); `);
await expect(page.locator('body')).toMatchAriaSnapshot(` await expect(page.locator('body')).toMatchAriaSnapshot(`
- group: Summary - group: "Summary"
`); `);
}); });

View file

@ -511,13 +511,13 @@ test('should support toHaveURL with baseURL from webServer', async ({ runInlineT
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
test('pass', async ({ page }) => { test('pass', async ({ page }) => {
await page.goto('/foobar'); await page.goto('/hello');
await expect(page).toHaveURL('/foobar'); await expect(page).toHaveURL('/hello');
await expect(page).toHaveURL('http://localhost:${port}/foobar'); await expect(page).toHaveURL('http://localhost:${port}/hello');
}); });
test('fail', async ({ page }) => { test('fail', async ({ page }) => {
await page.goto('/foobar'); await page.goto('/hello');
await expect(page).toHaveURL('/kek', { timeout: 1000 }); await expect(page).toHaveURL('/kek', { timeout: 1000 });
}); });
`, `,

View file

@ -192,6 +192,33 @@ test('should work with screenshot: only-on-failure', async ({ runInlineTest }, t
]); ]);
}); });
test('should work with screenshot: on-first-failure', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('fails', async ({ page }) => {
await page.setContent('I am the page');
expect(1).toBe(2);
});
`,
'playwright.config.ts': `
module.exports = {
retries: 1,
use: { screenshot: 'on-first-failure' }
};
`,
}, { workers: 1 });
expect(result.exitCode).toBe(1);
expect(result.passed).toBe(0);
expect(result.failed).toBe(1);
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
'.last-run.json',
'a-fails',
' test-failed-1.png',
]);
});
test('should work with screenshot: only-on-failure & fullPage', async ({ runInlineTest, server }, testInfo) => { test('should work with screenshot: only-on-failure & fullPage', async ({ runInlineTest, server }, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
'artifacts.spec.ts': ` 'artifacts.spec.ts': `

View file

@ -735,28 +735,34 @@ test('should not throw when attachment is missing', async ({ runInlineTest }, te
}); });
test('should not throw when screenshot on failure fails', async ({ runInlineTest, server }, testInfo) => { test('should not throw when screenshot on failure fails', async ({ runInlineTest, server }, testInfo) => {
server.setRoute('/download', (req, res) => {
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', 'attachment; filename=file.txt');
res.end(`Hello world`);
});
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.ts': ` 'playwright.config.ts': `
module.exports = { use: { trace: 'on', screenshot: 'on' } }; module.exports = { use: { trace: 'on', screenshot: 'on' } };
`, `,
'a.spec.ts': ` 'a.spec.ts': `
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
test('has pdf page', async ({ page }) => { test('has download page', async ({ page }) => {
await page.goto("${server.EMPTY_PAGE}"); await page.goto("${server.EMPTY_PAGE}");
await page.setContent('<a href="/empty.pdf" target="blank">open me!</a>'); await page.setContent('<a href="/download" target="blank">open me!</a>');
const downloadPromise = page.waitForEvent('download'); const downloadPromise = page.waitForEvent('download');
await page.click('a'); await page.click('a');
const download = await downloadPromise; const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('empty.pdf'); expect(download.suggestedFilename()).toBe('file.txt');
}); });
`, `,
}, { workers: 1 }); }, { workers: 1 });
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1); expect(result.passed).toBe(1);
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-has-pdf-page', 'trace.zip')); const trace = await parseTrace(testInfo.outputPath('test-results', 'a-has-download-page', 'trace.zip'));
const attachedScreenshots = trace.actionTree.filter(s => s.trim() === `attach "screenshot"`); const attachedScreenshots = trace.actionTree.filter(s => s.trim() === `attach "screenshot"`);
// One screenshot for the page, no screenshot for pdf page since it should have failed. // One screenshot for the page, no screenshot for the download page since it should have failed.
expect(attachedScreenshots.length).toBe(1); expect(attachedScreenshots.length).toBe(1);
}); });

View file

@ -65,19 +65,19 @@ test('should run visible', async ({ runUITest }) => {
- tree: - tree:
- treeitem "[icon-error] a.test.ts" [expanded]: - treeitem "[icon-error] a.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] passes \d+ms/} - treeitem ${/\[icon-check\] passes/}
- treeitem ${/\[icon-error\] fails \d+ms/} [selected]: - treeitem ${/\[icon-error\] fails/} [selected]:
- button "Run" - button "Run"
- button "Show source" - button "Show source"
- button "Watch" - button "Watch"
- treeitem "[icon-error] suite" - treeitem "[icon-error] suite"
- treeitem "[icon-error] b.test.ts" [expanded]: - treeitem "[icon-error] b.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] passes \d+ms/} - treeitem ${/\[icon-check\] passes/}
- treeitem ${/\[icon-error\] fails \d+ms/} - treeitem ${/\[icon-error\] fails/}
- treeitem "[icon-check] c.test.ts" [expanded]: - treeitem "[icon-check] c.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] passes \d+ms/} - treeitem ${/\[icon-check\] passes/}
- treeitem "[icon-circle-slash] skipped" - treeitem "[icon-circle-slash] skipped"
`); `);
@ -125,7 +125,7 @@ test('should run on hover', async ({ runUITest }) => {
- tree: - tree:
- treeitem "[icon-circle-outline] a.test.ts" [expanded]: - treeitem "[icon-circle-outline] a.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] passes \d+ms/}: - treeitem ${/\[icon-check\] passes/}:
- button "Run" - button "Run"
- button "Show source" - button "Show source"
- button "Watch" - button "Watch"
@ -185,7 +185,7 @@ test('should run on Enter', async ({ runUITest }) => {
- treeitem "[icon-error] a.test.ts" [expanded]: - treeitem "[icon-error] a.test.ts" [expanded]:
- group: - group:
- treeitem "[icon-circle-outline] passes" - treeitem "[icon-circle-outline] passes"
- treeitem ${/\[icon-error\] fails \d+ms/} [selected]: - treeitem ${/\[icon-error\] fails/} [selected]:
- button "Run" - button "Run"
- button "Show source" - button "Show source"
- button "Watch" - button "Watch"
@ -225,19 +225,19 @@ test('should run by project', async ({ runUITest }) => {
- tree: - tree:
- treeitem "[icon-error] a.test.ts" [expanded]: - treeitem "[icon-error] a.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] passes \d+ms/} - treeitem ${/\[icon-check\] passes/}
- treeitem ${/\[icon-error\] fails \d+ms/} [selected]: - treeitem ${/\[icon-error\] fails/} [selected]:
- button "Run" - button "Run"
- button "Show source" - button "Show source"
- button "Watch" - button "Watch"
- treeitem "[icon-error] suite" - treeitem "[icon-error] suite"
- treeitem "[icon-error] b.test.ts" [expanded]: - treeitem "[icon-error] b.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] passes \d+ms/} - treeitem ${/\[icon-check\] passes/}
- treeitem ${/\[icon-error\] fails \d+ms/} - treeitem ${/\[icon-error\] fails/}
- treeitem "[icon-check] c.test.ts" [expanded]: - treeitem "[icon-check] c.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] passes \d+ms/} - treeitem ${/\[icon-check\] passes/}
- treeitem "[icon-circle-slash] skipped" - treeitem "[icon-circle-slash] skipped"
`); `);
@ -299,14 +299,14 @@ test('should run by project', async ({ runUITest }) => {
- tree: - tree:
- treeitem "[icon-error] a.test.ts" [expanded]: - treeitem "[icon-error] a.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-circle-outline\] passes \d+ms/} [expanded] [selected]: - treeitem ${/\[icon-circle-outline\] passes/} [expanded] [selected]:
- button "Run" - button "Run"
- button "Show source" - button "Show source"
- button "Watch" - button "Watch"
- group: - group:
- treeitem ${/\[icon-check\] foo \d+ms/} - treeitem ${/\[icon-check\] foo/}
- treeitem ${/\[icon-circle-outline\] bar/} - treeitem ${/\[icon-circle-outline\] bar/}
- treeitem ${/\[icon-error\] fails \d+ms/} - treeitem ${/\[icon-error\] fails/}
`); `);
await expect(page.getByText('Projects: foo bar')).toBeVisible(); await expect(page.getByText('Projects: foo bar')).toBeVisible();
@ -333,17 +333,17 @@ test('should run by project', async ({ runUITest }) => {
- tree: - tree:
- treeitem "[icon-error] a.test.ts" [expanded]: - treeitem "[icon-error] a.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] passes \d+ms/} [expanded]: - treeitem ${/\[icon-check\] passes/} [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] foo \d+ms/} - treeitem ${/\[icon-check\] foo/}
- treeitem ${/\[icon-check\] bar \d+ms/} - treeitem ${/\[icon-check\] bar/}
- treeitem ${/\[icon-error\] fails \d+ms/} [expanded]: - treeitem ${/\[icon-error\] fails/} [expanded]:
- group: - group:
- treeitem ${/\[icon-error\] foo \d+ms/} [selected]: - treeitem ${/\[icon-error\] foo/} [selected]:
- button "Run" - button "Run"
- button "Show source" - button "Show source"
- button "Watch" - button "Watch"
- treeitem ${/\[icon-error\] bar \d+ms/} - treeitem ${/\[icon-error\] bar/}
- treeitem ${/\[icon-error\] suite/} - treeitem ${/\[icon-error\] suite/}
- treeitem "[icon-error] b.test.ts" [expanded]: - treeitem "[icon-error] b.test.ts" [expanded]:
- group: - group:
@ -385,7 +385,7 @@ test('should stop', async ({ runUITest }) => {
- treeitem "[icon-loading] a.test.ts" [expanded]: - treeitem "[icon-loading] a.test.ts" [expanded]:
- group: - group:
- treeitem "[icon-circle-slash] test 0" - treeitem "[icon-circle-slash] test 0"
- treeitem ${/\[icon-check\] test 1 \d+ms/} - treeitem ${/\[icon-check\] test 1/}
- treeitem ${/\[icon-loading\] test 2/} - treeitem ${/\[icon-loading\] test 2/}
- treeitem ${/\[icon-clock\] test 3/} - treeitem ${/\[icon-clock\] test 3/}
`); `);
@ -408,7 +408,7 @@ test('should stop', async ({ runUITest }) => {
- treeitem "[icon-circle-outline] a.test.ts" [expanded]: - treeitem "[icon-circle-outline] a.test.ts" [expanded]:
- group: - group:
- treeitem "[icon-circle-slash] test 0" - treeitem "[icon-circle-slash] test 0"
- treeitem ${/\[icon-check\] test 1 \d+ms/} - treeitem ${/\[icon-check\] test 1/}
- treeitem ${/\[icon-circle-outline\] test 2/} - treeitem ${/\[icon-circle-outline\] test 2/}
- treeitem ${/\[icon-circle-outline\] test 3/} - treeitem ${/\[icon-circle-outline\] test 3/}
`); `);
@ -478,19 +478,19 @@ test('should show time', async ({ runUITest }) => {
- tree: - tree:
- treeitem "[icon-error] a.test.ts" [expanded]: - treeitem "[icon-error] a.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] passes \d+ms/} - treeitem ${/\[icon-check\] passes \d+m?s/}
- treeitem ${/\[icon-error\] fails \d+ms/} [selected]: - treeitem ${/\[icon-error\] fails \d+m?s/} [selected]:
- button "Run" - button "Run"
- button "Show source" - button "Show source"
- button "Watch" - button "Watch"
- treeitem "[icon-error] suite" - treeitem "[icon-error] suite"
- treeitem "[icon-error] b.test.ts" [expanded]: - treeitem "[icon-error] b.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] passes \d+ms/} - treeitem ${/\[icon-check\] passes \d+m?s/}
- treeitem ${/\[icon-error\] fails \d+ms/} - treeitem ${/\[icon-error\] fails \d+m?s/}
- treeitem "[icon-check] c.test.ts" [expanded]: - treeitem "[icon-check] c.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] passes \d+ms/} - treeitem ${/\[icon-check\] passes \d+m?s/}
- treeitem "[icon-circle-slash] skipped" - treeitem "[icon-circle-slash] skipped"
`); `);
@ -522,7 +522,7 @@ test('should show test.fail as passing', async ({ runUITest }) => {
- tree: - tree:
- treeitem "[icon-check] a.test.ts" [expanded]: - treeitem "[icon-check] a.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] should fail \d+ms/} - treeitem ${/\[icon-check\] should fail \d+m?s/}
`); `);
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
@ -558,7 +558,7 @@ test('should ignore repeatEach', async ({ runUITest }) => {
- tree: - tree:
- treeitem "[icon-check] a.test.ts" [expanded]: - treeitem "[icon-check] a.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] should pass \d+ms/} - treeitem ${/\[icon-check\] should pass/}
`); `);
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
@ -593,7 +593,7 @@ test('should remove output folder before test run', async ({ runUITest }) => {
- tree: - tree:
- treeitem "[icon-check] a.test.ts" [expanded]: - treeitem "[icon-check] a.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] should pass \d+ms/} - treeitem ${/\[icon-check\] should pass/}
`); `);
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
@ -608,7 +608,7 @@ test('should remove output folder before test run', async ({ runUITest }) => {
- tree: - tree:
- treeitem "[icon-check] a.test.ts" [expanded]: - treeitem "[icon-check] a.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] should pass \d+ms/} - treeitem ${/\[icon-check\] should pass/}
`); `);
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
@ -656,7 +656,7 @@ test('should show proper total when using deps', async ({ runUITest }) => {
- tree: - tree:
- treeitem "[icon-circle-outline] a.test.ts" [expanded]: - treeitem "[icon-circle-outline] a.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] run @setup setup \d+ms/} [selected]: - treeitem ${/\[icon-check\] run @setup setup/} [selected]:
- button "Run" - button "Run"
- button "Show source" - button "Show source"
- button "Watch" - button "Watch"
@ -676,8 +676,8 @@ test('should show proper total when using deps', async ({ runUITest }) => {
- tree: - tree:
- treeitem "[icon-check] a.test.ts" [expanded]: - treeitem "[icon-check] a.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] run @setup setup \d+ms/} - treeitem ${/\[icon-check\] run @setup setup/}
- treeitem ${/\[icon-check\] run @chromium chromium \d+ms/} [selected]: - treeitem ${/\[icon-check\] run @chromium chromium/} [selected]:
- button "Run" - button "Run"
- button "Show source" - button "Show source"
- button "Watch" - button "Watch"
@ -746,7 +746,7 @@ test('should respect --tsconfig option', {
- tree: - tree:
- treeitem "[icon-check] a.test.ts" [expanded]: - treeitem "[icon-check] a.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] test \d+ms/} - treeitem ${/\[icon-check\] test/}
`); `);
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
@ -775,6 +775,6 @@ test('should respect --ignore-snapshots option', {
- tree: - tree:
- treeitem "[icon-check] a.test.ts" [expanded]: - treeitem "[icon-check] a.test.ts" [expanded]:
- group: - group:
- treeitem ${/\[icon-check\] snapshot \d+ms/} - treeitem ${/\[icon-check\] snapshot/}
`); `);
}); });

View file

@ -276,7 +276,7 @@ test('should restart webserver on reload', async ({ runUITest }) => {
'a.test.js': ` 'a.test.js': `
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
test('should work', async ({ page }) => { test('should work', async ({ page }) => {
await page.goto('http://localhost:${port}'); await page.goto('http://localhost:${port}/hello');
}); });
` `
}, { DEBUG: 'pw:webserver' }); }, { DEBUG: 'pw:webserver' });

View file

@ -0,0 +1,48 @@
/**
* 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 * as fs from 'fs';
import { test, expect } from './playwright-test-fixtures';
test('should update snapshot with the update-snapshots flag', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot(\`
- heading "world"
\`);
});
`
}, { 'update-snapshots': true });
expect(result.exitCode).toBe(0);
const patchPath = testInfo.outputPath('test-results/rebaselines.patch');
const data = fs.readFileSync(patchPath, 'utf-8');
expect(data).toBe(`--- a/a.spec.ts
+++ b/a.spec.ts
@@ -3,7 +3,7 @@
test('test', async ({ page }) => {
await page.setContent(\`<h1>hello</h1>\`);
await expect(page.locator('body')).toMatchAriaSnapshot(\`
- - heading "world"
+ - heading "hello" [level=1]
\`);
});
`);
});

View file

@ -236,7 +236,7 @@ export interface PlaywrightWorkerOptions {
video: VideoMode | /** deprecated */ 'retry-with-video' | { mode: VideoMode, size?: ViewportSize }; video: VideoMode | /** deprecated */ 'retry-with-video' | { mode: VideoMode, size?: ViewportSize };
} }
export type ScreenshotMode = 'off' | 'on' | 'only-on-failure'; export type ScreenshotMode = 'off' | 'on' | 'only-on-failure' | 'on-first-failure';
export type TraceMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'on-all-retries' | 'retain-on-first-failure'; export type TraceMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'on-all-retries' | 'retain-on-first-failure';
export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry'; export type VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry';