Merge branch 'main' into extend-type-accessible-name
This commit is contained in:
commit
8224c93cdf
21
.github/workflows/tests_secondary.yml
vendored
21
.github/workflows/tests_secondary.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# 🎭 Playwright
|
# 🎭 Playwright
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||||
|
|
||||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
||||||
|
|
||||||
| | Linux | macOS | Windows |
|
| | Linux | macOS | Windows |
|
||||||
| :--- | :---: | :---: | :---: |
|
| :--- | :---: | :---: | :---: |
|
||||||
| Chromium <!-- GEN:chromium-version -->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: |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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({}));
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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') {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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':
|
||||||
|
|
|
||||||
127
packages/playwright-core/src/utils/patch.ts
Normal file
127
packages/playwright-core/src/utils/patch.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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][];
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
95
packages/playwright/src/runner/rebase.ts
Normal file
95
packages/playwright/src/runner/rebase.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@
|
||||||
../transform/
|
../transform/
|
||||||
../util.ts
|
../util.ts
|
||||||
../utilBundle.ts
|
../utilBundle.ts
|
||||||
|
../matchers/**
|
||||||
|
|
|
||||||
|
|
@ -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')}`;
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
3
packages/playwright/types/test.d.ts
vendored
3
packages/playwright/types/test.d.ts
vendored
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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' }],
|
||||||
|
|
|
||||||
|
|
@ -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'));
|
||||||
|
|
|
||||||
|
|
@ -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`);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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`);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
260
tests/library/unit/patch.spec.ts
Normal file
260
tests/library/unit/patch.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
|
@ -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 = [];
|
||||||
|
|
@ -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"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
|
|
|
||||||
|
|
@ -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': `
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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/}
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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' });
|
||||||
|
|
|
||||||
48
tests/playwright-test/update-aria-snapshot.spec.ts
Normal file
48
tests/playwright-test/update-aria-snapshot.spec.ts
Normal 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]
|
||||||
|
\`);
|
||||||
|
});
|
||||||
|
|
||||||
|
`);
|
||||||
|
});
|
||||||
2
utils/generate_types/overrides-test.d.ts
vendored
2
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue