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 }}
|
||||
env:
|
||||
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
|
||||
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
|
||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|||
|
||||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->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: |
|
||||
| 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
|
||||
* since: v1.10
|
||||
- type: <[Object]|[ScreenshotMode]<"off"|"on"|"only-on-failure">>
|
||||
- `mode` <[ScreenshotMode]<"off"|"on"|"only-on-failure">> Automatic screenshot mode.
|
||||
- type: <[Object]|[ScreenshotMode]<"off"|"on"|"only-on-failure"|"on-first-failure">>
|
||||
- `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`.
|
||||
- `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.
|
||||
* `'on'`: Capture screenshot after each test.
|
||||
* `'only-on-failure'`: Capture screenshot after each test failure.
|
||||
* `'on-first-failure'`: Capture screenshot after each test's first failure.
|
||||
|
||||
**Usage**
|
||||
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ See [Running Tests](./running-tests.md) for general information on `pytest` opti
|
|||
|
||||
## Examples
|
||||
|
||||
### Configure Mypy typings for auto-completion
|
||||
### Configure typings for auto-completion
|
||||
|
||||
```py title="test_my_application.py"
|
||||
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
|
||||
|
||||
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)
|
||||
- debug@4.3.4 (https://github.com/debug-js/debug)
|
||||
- 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)
|
||||
- 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)
|
||||
|
|
@ -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
|
||||
|
||||
%% 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
|
||||
=========================================
|
||||
Copyright (c) 2015, Scott Motte
|
||||
|
|
@ -1194,6 +1401,6 @@ END OF yazl@2.5.1 AND INFORMATION
|
|||
|
||||
SUMMARY BEGIN HERE
|
||||
=========================================
|
||||
Total Packages: 47
|
||||
Total Packages: 48
|
||||
=========================================
|
||||
END OF SUMMARY
|
||||
|
|
@ -3,9 +3,9 @@
|
|||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1143",
|
||||
"revision": "1145",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "131.0.6778.3"
|
||||
"browserVersion": "131.0.6778.13"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"colors": "1.4.0",
|
||||
"commander": "8.3.0",
|
||||
"debug": "^4.3.4",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"graceful-fs": "4.2.10",
|
||||
"https-proxy-agent": "7.0.5",
|
||||
|
|
@ -30,6 +31,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/minimatch": "^3.0.5",
|
||||
"@types/pngjs": "^6.0.1",
|
||||
|
|
@ -49,6 +51,12 @@
|
|||
"@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": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
|
||||
|
|
@ -201,6 +209,11 @@
|
|||
"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": {
|
||||
"version": "16.4.5",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||
|
|
@ -456,6 +469,12 @@
|
|||
"@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": {
|
||||
"version": "2.0.3",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "16.4.5",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"colors": "1.4.0",
|
||||
"commander": "8.3.0",
|
||||
"debug": "^4.3.4",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"graceful-fs": "4.2.10",
|
||||
"https-proxy-agent": "7.0.5",
|
||||
|
|
@ -26,11 +27,12 @@
|
|||
"signal-exit": "3.0.7",
|
||||
"socks-proxy-agent": "8.0.4",
|
||||
"stack-utils": "2.0.5",
|
||||
"yaml": "^2.5.1",
|
||||
"ws": "8.17.1"
|
||||
"ws": "8.17.1",
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/minimatch": "^3.0.5",
|
||||
"@types/pngjs": "^6.0.1",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ export const colors = colorsLibrary;
|
|||
import debugLibrary from 'debug';
|
||||
export const debug = debugLibrary;
|
||||
|
||||
import diffMatchPatchLibrary from 'diff-match-patch';
|
||||
export const diffMatchPatch = diffMatchPatchLibrary;
|
||||
|
||||
import dotenvLibrary from 'dotenv';
|
||||
export const dotenv = dotenvLibrary;
|
||||
|
||||
|
|
|
|||
|
|
@ -554,6 +554,7 @@ async function open(options: Options, url: string | undefined, language: string)
|
|||
contextOptions,
|
||||
device: options.device,
|
||||
saveStorage: options.saveStorage,
|
||||
handleSIGINT: false,
|
||||
});
|
||||
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',
|
||||
testIdAttributeName,
|
||||
outputFile: outputFile ? path.resolve(outputFile) : undefined,
|
||||
handleSIGINT: false,
|
||||
});
|
||||
await openPage(context, url);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -976,6 +976,7 @@ scheme.BrowserContextEnableRecorderParams = tObject({
|
|||
device: tOptional(tString),
|
||||
saveStorage: tOptional(tString),
|
||||
outputFile: tOptional(tString),
|
||||
handleSIGINT: tOptional(tBoolean),
|
||||
omitCallTracking: tOptional(tBoolean),
|
||||
});
|
||||
scheme.BrowserContextEnableRecorderResult = tOptional(tObject({}));
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Galaxy S5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S8": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 740
|
||||
|
|
@ -143,7 +143,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S8 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 740,
|
||||
"height": 360
|
||||
|
|
@ -154,7 +154,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S9+": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 320,
|
||||
"height": 658
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S9+ landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 658,
|
||||
"height": 320
|
||||
|
|
@ -176,7 +176,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy Tab S4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 712,
|
||||
"height": 1138
|
||||
|
|
@ -187,7 +187,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy Tab S4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1138,
|
||||
"height": 712
|
||||
|
|
@ -1098,7 +1098,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"LG Optimus L70": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/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": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1109,7 +1109,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"LG Optimus L70 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1120,7 +1120,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 550": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1131,7 +1131,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 550 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1142,7 +1142,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 950": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1153,7 +1153,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Microsoft Lumia 950 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1164,7 +1164,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 10": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 800,
|
||||
"height": 1280
|
||||
|
|
@ -1175,7 +1175,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 10 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1280,
|
||||
"height": 800
|
||||
|
|
@ -1186,7 +1186,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1197,7 +1197,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1208,7 +1208,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1219,7 +1219,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1230,7 +1230,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5X": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1241,7 +1241,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5X landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1252,7 +1252,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1263,7 +1263,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1274,7 +1274,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6P": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1285,7 +1285,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6P landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1296,7 +1296,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 7": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 600,
|
||||
"height": 960
|
||||
|
|
@ -1307,7 +1307,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 7 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 960,
|
||||
"height": 600
|
||||
|
|
@ -1362,7 +1362,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Pixel 2": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 411,
|
||||
"height": 731
|
||||
|
|
@ -1373,7 +1373,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 731,
|
||||
"height": 411
|
||||
|
|
@ -1384,7 +1384,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 XL": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 411,
|
||||
"height": 823
|
||||
|
|
@ -1395,7 +1395,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 XL landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 823,
|
||||
"height": 411
|
||||
|
|
@ -1406,7 +1406,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 3": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 393,
|
||||
"height": 786
|
||||
|
|
@ -1417,7 +1417,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 3 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 786,
|
||||
"height": 393
|
||||
|
|
@ -1428,7 +1428,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 353,
|
||||
"height": 745
|
||||
|
|
@ -1439,7 +1439,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 745,
|
||||
"height": 353
|
||||
|
|
@ -1450,7 +1450,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4a (5G)": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 892
|
||||
|
|
@ -1465,7 +1465,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4a (5G) landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"height": 892,
|
||||
"width": 412
|
||||
|
|
@ -1480,7 +1480,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 393,
|
||||
"height": 851
|
||||
|
|
@ -1495,7 +1495,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 851,
|
||||
"height": 393
|
||||
|
|
@ -1510,7 +1510,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 7": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 412,
|
||||
"height": 915
|
||||
|
|
@ -1525,7 +1525,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 7 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 915,
|
||||
"height": 412
|
||||
|
|
@ -1540,7 +1540,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Moto G4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1551,7 +1551,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Moto G4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1562,7 +1562,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Chrome HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1577,7 +1577,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Edge HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1622,7 +1622,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Desktop Chrome": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
@ -1637,7 +1637,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Edge": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
|
|||
|
|
@ -14,9 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { escapeWithQuotes } from '@isomorphic/stringUtils';
|
||||
import * as roleUtils from './roleUtils';
|
||||
import { isElementVisible, isElementStyleVisibilityVisible, getElementComputedStyle } from './domUtils';
|
||||
import { getElementComputedStyle } from './domUtils';
|
||||
import type { AriaRole } from './roleUtils';
|
||||
|
||||
type AriaProps = {
|
||||
|
|
@ -29,7 +28,7 @@ type AriaProps = {
|
|||
};
|
||||
|
||||
type AriaNode = AriaProps & {
|
||||
role: AriaRole | 'fragment' | 'text';
|
||||
role: AriaRole | 'fragment';
|
||||
name: string;
|
||||
children: (AriaNode | string)[];
|
||||
};
|
||||
|
|
@ -56,22 +55,10 @@ export function generateAriaTree(rootElement: Element): AriaNode {
|
|||
if (roleUtils.isElementHiddenForAria(element))
|
||||
return;
|
||||
|
||||
const visible = isElementVisible(element);
|
||||
const hasVisibleChildren = isElementStyleVisibilityVisible(element);
|
||||
|
||||
if (!hasVisibleChildren)
|
||||
return;
|
||||
|
||||
if (visible) {
|
||||
const childAriaNode = toAriaNode(element);
|
||||
const isHiddenContainer = childAriaNode && hiddenContainerRoles.has(childAriaNode.ariaNode.role);
|
||||
if (childAriaNode && !isHiddenContainer)
|
||||
ariaNode.children.push(childAriaNode.ariaNode);
|
||||
if (isHiddenContainer || !childAriaNode?.isLeaf)
|
||||
processChildNodes(childAriaNode?.ariaNode || ariaNode, element);
|
||||
} else {
|
||||
processChildNodes(ariaNode, element);
|
||||
}
|
||||
if (childAriaNode)
|
||||
ariaNode.children.push(childAriaNode);
|
||||
processChildNodes(childAriaNode || ariaNode, element);
|
||||
};
|
||||
|
||||
function processChildNodes(ariaNode: AriaNode, element: Element) {
|
||||
|
|
@ -101,6 +88,9 @@ export function generateAriaTree(rootElement: Element): AriaNode {
|
|||
|
||||
if (treatAsBlock)
|
||||
ariaNode.children.push(treatAsBlock);
|
||||
|
||||
if (ariaNode.children.length === 1 && ariaNode.name === ariaNode.children[0])
|
||||
ariaNode.children = [];
|
||||
}
|
||||
|
||||
roleUtils.beginAriaCaches();
|
||||
|
|
@ -115,19 +105,13 @@ export function generateAriaTree(rootElement: Element): AriaNode {
|
|||
return ariaRoot;
|
||||
}
|
||||
|
||||
function toAriaNode(element: Element): { ariaNode: AriaNode, isLeaf: boolean } | null {
|
||||
function toAriaNode(element: Element): AriaNode | null {
|
||||
const role = roleUtils.getAriaRole(element);
|
||||
if (!role)
|
||||
return null;
|
||||
|
||||
const name = roleUtils.getElementAccessibleName(element, false) || '';
|
||||
const isLeaf = leafRoles.has(role);
|
||||
const result: AriaNode = { role, name, children: [] };
|
||||
if (isLeaf && !name) {
|
||||
const text = roleUtils.accumulatedElementText(element);
|
||||
if (text)
|
||||
result.children = [text];
|
||||
}
|
||||
|
||||
if (roleUtils.kAriaCheckedRoles.includes(role))
|
||||
result.checked = roleUtils.getAriaChecked(element);
|
||||
|
|
@ -147,7 +131,7 @@ function toAriaNode(element: Element): { ariaNode: AriaNode, isLeaf: boolean } |
|
|||
if (roleUtils.kAriaSelectedRoles.includes(role))
|
||||
result.selected = roleUtils.getAriaSelected(element);
|
||||
|
||||
return { isLeaf, ariaNode: result };
|
||||
return result;
|
||||
}
|
||||
|
||||
export function renderedAriaTree(rootElement: Element): string {
|
||||
|
|
@ -178,21 +162,12 @@ function normalizeStringChildren(rootA11yNode: AriaNode) {
|
|||
}
|
||||
flushChildren(buffer, normalizedChildren);
|
||||
ariaNode.children = normalizedChildren.length ? normalizedChildren : [];
|
||||
if (ariaNode.children.length === 1 && ariaNode.children[0] === ariaNode.name)
|
||||
ariaNode.children = [];
|
||||
};
|
||||
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, ' ');
|
||||
|
||||
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 } {
|
||||
const root = generateAriaTree(rootElement);
|
||||
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 {
|
||||
|
|
@ -276,17 +251,16 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode): boolean {
|
|||
return !!results.length;
|
||||
}
|
||||
|
||||
export function renderAriaTree(ariaNode: AriaNode, options?: { noText?: boolean }): string {
|
||||
export function renderAriaTree(ariaNode: AriaNode): string {
|
||||
const lines: string[] = [];
|
||||
const visit = (ariaNode: AriaNode | string, indent: string) => {
|
||||
if (typeof ariaNode === 'string') {
|
||||
if (!options?.noText)
|
||||
lines.push(indent + '- text: ' + quoteYamlString(ariaNode));
|
||||
return;
|
||||
}
|
||||
let line = `${indent}- ${ariaNode.role}`;
|
||||
if (ariaNode.name)
|
||||
line += ` ${escapeWithQuotes(ariaNode.name, '"')}`;
|
||||
line += ` ${quoteYamlString(ariaNode.name)}`;
|
||||
|
||||
if (ariaNode.checked === 'mixed')
|
||||
line += ` [checked=mixed]`;
|
||||
|
|
@ -305,17 +279,16 @@ export function renderAriaTree(ariaNode: AriaNode, options?: { noText?: boolean
|
|||
if (ariaNode.selected === true)
|
||||
line += ` [selected]`;
|
||||
|
||||
const stringValue = !ariaNode.children.length || (ariaNode.children?.length === 1 && typeof ariaNode.children[0] === 'string');
|
||||
if (stringValue) {
|
||||
if (!options?.noText && ariaNode.children.length)
|
||||
line += ': ' + quoteYamlString(ariaNode.children?.[0] as string);
|
||||
if (!ariaNode.children.length) {
|
||||
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 + ':');
|
||||
for (const child of ariaNode.children || [])
|
||||
visit(child, indent + ' ');
|
||||
}
|
||||
};
|
||||
|
||||
if (ariaNode.role === 'fragment') {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { buildFullSelector } from '../utils/isomorphic/recorderUtils';
|
|||
const recorderSymbol = Symbol('recorderSymbol');
|
||||
|
||||
export class Recorder implements InstrumentationListener, IRecorder {
|
||||
readonly handleSIGINT: boolean | undefined;
|
||||
private _context: BrowserContext;
|
||||
private _mode: Mode;
|
||||
private _highlightedSelector = '';
|
||||
|
|
@ -75,6 +76,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
|
|||
|
||||
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
|
||||
this._mode = params.mode || 'none';
|
||||
this.handleSIGINT = params.handleSIGINT;
|
||||
this._contextRecorder = new ContextRecorder(codegenMode, context, params, {});
|
||||
this._context = context;
|
||||
this._omitCallTracking = !!params.omitCallTracking;
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
|
|||
noDefaultViewport: true,
|
||||
headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed),
|
||||
useWebSocket: isUnderTest(),
|
||||
handleSIGINT: false,
|
||||
handleSIGINT: recorder.handleSIGINT,
|
||||
executablePath: inspectedContext._browser.options.isChromium ? inspectedContext._browser.options.customExecutablePath : undefined,
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import type { EventEmitter } from 'events';
|
|||
export interface IRecorder {
|
||||
setMode(mode: Mode): void;
|
||||
mode(): Mode;
|
||||
readonly handleSIGINT: boolean | undefined;
|
||||
}
|
||||
|
||||
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
|
||||
../image_tools/compare.ts
|
||||
../utilsBundle.ts
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
import { colors, jpegjs } from '../utilsBundle';
|
||||
const pixelmatch = require('../third_party/pixelmatch');
|
||||
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';
|
||||
|
||||
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 {
|
||||
const { diff_match_patch } = diffMatchPatch;
|
||||
if (typeof actual !== 'string')
|
||||
return { errorMessage: 'Actual result should be a string' };
|
||||
const expected = expectedBuffer.toString('utf-8');
|
||||
|
|
@ -120,6 +121,7 @@ function compareText(actual: Buffer | string, expectedBuffer: Buffer): Comparato
|
|||
}
|
||||
|
||||
function diff_prettyTerminal(diffs: [number, string][]) {
|
||||
const { DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL } = diffMatchPatch;
|
||||
const html = [];
|
||||
for (let x = 0; x < diffs.length; x++) {
|
||||
const op = diffs[x][0]; // Operation (insert, delete, equal)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export * from './isomorphic/stringUtils';
|
|||
export * from './isomorphic/urlMatch';
|
||||
export * from './multimap';
|
||||
export * from './network';
|
||||
export * from './patch';
|
||||
export * from './processLauncher';
|
||||
export * from './profiler';
|
||||
export * from './rtti';
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { type NestedSelectorBody, parseAttributeSelector, parseSelector, stringi
|
|||
import type { ParsedSelector } from './selectorParser';
|
||||
|
||||
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 Quote = '\'' | '"' | '`';
|
||||
|
||||
|
|
@ -158,19 +158,29 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
|
|||
}
|
||||
}
|
||||
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';
|
||||
continue;
|
||||
}
|
||||
|
||||
const locatorType: LocatorType = 'default';
|
||||
|
||||
const nextPart = parts[index + 1];
|
||||
|
||||
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);
|
||||
// There is no locator equivalent for strict has-text and has-not-text, leave it as is.
|
||||
if (!exact) {
|
||||
|
|
@ -194,7 +204,7 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
|
|||
let locatorPartWithEngine: string | undefined;
|
||||
if (['xpath', 'css'].includes(part.name)) {
|
||||
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[]);
|
||||
|
|
@ -253,6 +263,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
|||
if (options.hasNotText !== undefined)
|
||||
return `locator(${this.quote(body as string)}, { hasNotText: ${this.toHasText(options.hasNotText)} })`;
|
||||
return `locator(${this.quote(body as string)})`;
|
||||
case 'frame-locator':
|
||||
return `frameLocator(${this.quote(body as string)})`;
|
||||
case 'frame':
|
||||
return `contentFrame()`;
|
||||
case 'nth':
|
||||
|
|
@ -345,6 +357,8 @@ export class PythonLocatorFactory implements LocatorFactory {
|
|||
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)})`;
|
||||
case 'frame-locator':
|
||||
return `frame_locator(${this.quote(body as string)})`;
|
||||
case 'frame':
|
||||
return `content_frame`;
|
||||
case 'nth':
|
||||
|
|
@ -450,6 +464,8 @@ export class JavaLocatorFactory implements LocatorFactory {
|
|||
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)})`;
|
||||
case 'frame-locator':
|
||||
return `frameLocator(${this.quote(body as string)})`;
|
||||
case 'frame':
|
||||
return `contentFrame()`;
|
||||
case 'nth':
|
||||
|
|
@ -545,6 +561,8 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
|||
if (options.hasNotText !== undefined)
|
||||
return `Locator(${this.quote(body as string)}, new() { ${this.toHasNotText(options.hasNotText)} })`;
|
||||
return `Locator(${this.quote(body as string)})`;
|
||||
case 'frame-locator':
|
||||
return `FrameLocator(${this.quote(body as string)})`;
|
||||
case 'frame':
|
||||
return `ContentFrame`;
|
||||
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 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 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;
|
||||
|
|
@ -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 = new StackUtils({ internals: StackUtils.nodeInternals() });
|
||||
const nodeInternals = StackUtils.nodeInternals();
|
||||
const nodeMajorVersion = +process.versions.node.split('.')[0];
|
||||
|
||||
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);
|
||||
if (!frame)
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"@babel/code-frame": "^7.24.2",
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/helper-plugin-utils": "^7.24.0",
|
||||
"@babel/parser": "^7.24.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.24.1",
|
||||
"@babel/plugin-proposal-explicit-resource-management": "^7.24.1",
|
||||
"@babel/plugin-syntax-async-generators": "^7.8.4",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"@babel/code-frame": "^7.24.2",
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/helper-plugin-utils": "^7.24.0",
|
||||
"@babel/parser": "^7.24.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.24.1",
|
||||
"@babel/plugin-proposal-explicit-resource-management": "^7.24.1",
|
||||
"@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 { declare } from '@babel/helper-plugin-utils';
|
||||
export { types } from '@babel/core';
|
||||
export { parse } from '@babel/parser';
|
||||
import traverseFunction from '@babel/traverse';
|
||||
export const traverse = traverseFunction;
|
||||
|
||||
|
|
|
|||
|
|
@ -378,11 +378,6 @@ export function restartWithExperimentalTsEsm(configFile: string | undefined, for
|
|||
|
||||
// Now check for the newer API presence.
|
||||
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.
|
||||
const configIsModule = !!configFile && fileIsModule(configFile);
|
||||
if (!force && !configIsModule)
|
||||
|
|
|
|||
|
|
@ -15,9 +15,11 @@
|
|||
*/
|
||||
|
||||
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 { ReporterDescription, TestInfoError, TestStatus } from '../../types/test';
|
||||
import type { MatcherResultProperty } from '../matchers/matcherHint';
|
||||
|
||||
export type ConfigCLIOverrides = {
|
||||
debug?: boolean;
|
||||
|
|
@ -74,11 +76,15 @@ export type AttachmentPayload = {
|
|||
contentType: string;
|
||||
};
|
||||
|
||||
export type TestInfoErrorImpl = TestInfoError & {
|
||||
matcherResult?: MatcherResultProperty;
|
||||
};
|
||||
|
||||
export type TestEndPayload = {
|
||||
testId: string;
|
||||
duration: number;
|
||||
status: TestStatus;
|
||||
errors: TestInfoError[];
|
||||
errors: TestInfoErrorImpl[];
|
||||
hasNonRetriableError: boolean;
|
||||
expectedStatus: TestStatus;
|
||||
annotations: { type: string, description?: string }[];
|
||||
|
|
@ -99,7 +105,8 @@ export type StepEndPayload = {
|
|||
testId: string;
|
||||
stepId: string;
|
||||
wallTime: number; // milliseconds since unix epoch
|
||||
error?: TestInfoError;
|
||||
error?: TestInfoErrorImpl;
|
||||
suggestedRebaseline?: string;
|
||||
};
|
||||
|
||||
export type TestEntry = {
|
||||
|
|
@ -113,7 +120,7 @@ export type RunPayload = {
|
|||
};
|
||||
|
||||
export type DonePayload = {
|
||||
fatalErrors: TestInfoError[];
|
||||
fatalErrors: TestInfoErrorImpl[];
|
||||
skipTestsDueToSetupFailure: string[]; // test ids
|
||||
fatalUnknownTestIds?: string[];
|
||||
};
|
||||
|
|
@ -124,7 +131,7 @@ export type TestOutputPayload = {
|
|||
};
|
||||
|
||||
export type TeardownErrorsPayload = {
|
||||
fatalErrors: TestInfoError[];
|
||||
fatalErrors: TestInfoErrorImpl[];
|
||||
};
|
||||
|
||||
export type EnvProducedPayload = [string, string | null][];
|
||||
|
|
|
|||
|
|
@ -14,9 +14,8 @@
|
|||
* 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 type { TestInfoError } from '../../types/test';
|
||||
import { serializeError } from '../util';
|
||||
import { registerESMLoader } from './esmLoaderHost';
|
||||
import { execArgvWithoutExperimentalLoaderOptions } from '../transform/esmUtils';
|
||||
|
|
@ -29,7 +28,7 @@ export type ProtocolRequest = {
|
|||
|
||||
export type ProtocolResponse = {
|
||||
id?: number;
|
||||
error?: TestInfoError;
|
||||
error?: TestInfoErrorImpl;
|
||||
method?: string;
|
||||
params?: any;
|
||||
result?: any;
|
||||
|
|
|
|||
|
|
@ -571,7 +571,7 @@ class ArtifactsRecorder {
|
|||
if (this._reusedContexts.has(context))
|
||||
return;
|
||||
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
|
||||
// after the test finishes.
|
||||
await Promise.all(context.pages().map(page => this._screenshotPage(page, true)));
|
||||
|
|
@ -588,14 +588,19 @@ class ArtifactsRecorder {
|
|||
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() {
|
||||
const captureScreenshots = this._screenshotMode === 'on' || (this._screenshotMode === 'only-on-failure' && this._testInfo._isFailure());
|
||||
if (captureScreenshots)
|
||||
if (this._shouldCaptureScreenshotUponFinish())
|
||||
await this._screenshotOnTestFailure();
|
||||
}
|
||||
|
||||
async didFinishTest() {
|
||||
const captureScreenshots = this._screenshotMode === 'on' || (this._screenshotMode === 'only-on-failure' && this._testInfo._isFailure());
|
||||
const captureScreenshots = this._shouldCaptureScreenshotUponFinish();
|
||||
if (captureScreenshots)
|
||||
await this._screenshotOnTestFailure();
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ import {
|
|||
} from '../common/expectBundle';
|
||||
import { zones } from 'playwright-core/lib/utils';
|
||||
import { TestInfoImpl } from '../worker/testInfo';
|
||||
import { ExpectError, isExpectError } from './matcherHint';
|
||||
import { ExpectError, isJestError } from './matcherHint';
|
||||
import { toMatchAriaSnapshot } from './toMatchAriaSnapshot';
|
||||
|
||||
// #region
|
||||
|
|
@ -323,8 +323,13 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
|||
|
||||
const step = testInfo._addStep(stepInfo);
|
||||
|
||||
const reportStepError = (jestError: Error | unknown) => {
|
||||
const error = isExpectError(jestError) ? new ExpectError(jestError, customMessage, stackFrames) : jestError;
|
||||
const reportStepError = (e: Error | unknown) => {
|
||||
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 });
|
||||
if (this._info.isSoft)
|
||||
testInfo._failWithError(error);
|
||||
|
|
|
|||
|
|
@ -43,20 +43,21 @@ export type MatcherResult<E, A> = {
|
|||
printedReceived?: string;
|
||||
printedExpected?: 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 {
|
||||
matcherResult: {
|
||||
message: string;
|
||||
pass: boolean;
|
||||
name?: string;
|
||||
expected?: any;
|
||||
actual?: any;
|
||||
log?: string[];
|
||||
timeout?: number;
|
||||
};
|
||||
matcherResult: MatcherResultProperty;
|
||||
|
||||
constructor(jestError: ExpectError, customMessage: string, stackFrames: StackFrame[]) {
|
||||
constructor(jestError: JestError, customMessage: string, stackFrames: StackFrame[]) {
|
||||
super('');
|
||||
// Copy to erase the JestMatcherError constructor name from the console.log(error).
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { colors } from 'playwright-core/lib/utilsBundle';
|
|||
import { EXPECTED_COLOR } from '../common/expectBundle';
|
||||
import { callLogText } from '../util';
|
||||
import { printReceivedStringContainExpectedSubstring } from './expect';
|
||||
import { currentTestInfo } from '../common/globals';
|
||||
|
||||
export async function toMatchAriaSnapshot(
|
||||
this: ExpectMatcherState,
|
||||
|
|
@ -31,6 +32,15 @@ export async function toMatchAriaSnapshot(
|
|||
): Promise<MatcherResult<string | RegExp, string>> {
|
||||
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 = {
|
||||
isNot: this.isNot,
|
||||
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 {
|
||||
name: matcherName,
|
||||
expected,
|
||||
|
|
@ -72,6 +88,7 @@ export async function toMatchAriaSnapshot(
|
|||
pass,
|
||||
actual: received,
|
||||
log,
|
||||
suggestedRebaseline,
|
||||
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')}`);
|
||||
}
|
||||
|
||||
function unshift(snapshot: string): string {
|
||||
function unshift(snapshot: string, indent: string = ''): string {
|
||||
const lines = snapshot.split('\n');
|
||||
let whitespacePrefixLength = 100;
|
||||
for (const line of lines) {
|
||||
|
|
@ -91,5 +108,5 @@ function unshift(snapshot: string): string {
|
|||
whitespacePrefixLength = match[1].length;
|
||||
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 { FailureTracker } from './failureTracker';
|
||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||
import { addSuggestedRebaseline } from './rebase';
|
||||
|
||||
export type EnvByProjectId = Map<string, Record<string, string | undefined>>;
|
||||
|
||||
|
|
@ -341,6 +342,8 @@ class JobDispatcher {
|
|||
step.duration = params.wallTime - step.startTime.getTime();
|
||||
if (params.error)
|
||||
step.error = params.error;
|
||||
if (params.suggestedRebaseline)
|
||||
addSuggestedRebaseline(step.location!, params.suggestedRebaseline);
|
||||
steps.delete(params.stepId);
|
||||
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 { InternalReporter } from '../reporters/internalReporter';
|
||||
import { LastRunReporter } from './lastRun';
|
||||
import { applySuggestedRebaselines } from './rebase';
|
||||
|
||||
type ProjectConfigWithFiles = {
|
||||
name: string;
|
||||
|
|
@ -88,6 +89,8 @@ export class Runner {
|
|||
];
|
||||
const status = await runTasks(new TestRun(config, reporter), tasks, config.config.globalTimeout);
|
||||
|
||||
await applySuggestedRebaselines(config);
|
||||
|
||||
// Calling process.exit() might truncate large stdout/stderr output.
|
||||
// See https://github.com/nodejs/node/issues/6456.
|
||||
// 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 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 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 type BabelPlugin = [string, any?];
|
||||
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 { debug, mime, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle';
|
||||
import { formatCallLog } from 'playwright-core/lib/utils';
|
||||
import type { TestInfoError } from './../types/test';
|
||||
import type { Location } from './../types/testReporter';
|
||||
import { calculateSha1, isRegExp, isString, sanitizeForFilePath, stringifyStackFrames } 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_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json'));
|
||||
|
|
@ -62,7 +62,7 @@ export function filteredStackTrace(rawStack: RawStack): StackFrame[] {
|
|||
return frames;
|
||||
}
|
||||
|
||||
export function serializeError(error: Error | any): TestInfoError {
|
||||
export function serializeError(error: Error | any): TestInfoErrorImpl {
|
||||
if (error instanceof Error)
|
||||
return filterStackTrace(error);
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@
|
|||
../transform/
|
||||
../util.ts
|
||||
../utilBundle.ts
|
||||
../matchers/**
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { captureRawStack, monotonicTime, zones, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils';
|
||||
import type { TestInfoError, TestInfo, TestStatus, FullProject } from '../../types/test';
|
||||
import type { AttachmentPayload, StepBeginPayload, StepEndPayload, WorkerInitParams } from '../common/ipc';
|
||||
import type { TestInfo, TestStatus, FullProject } from '../../types/test';
|
||||
import type { AttachmentPayload, StepBeginPayload, StepEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc';
|
||||
import type { TestCase } from '../common/test';
|
||||
import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
|
||||
import type { RunnableDescription } from './timeoutManager';
|
||||
|
|
@ -28,10 +28,10 @@ import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normal
|
|||
import { TestTracing } from './testTracing';
|
||||
import type { Attachment } from './testTracing';
|
||||
import type { StackFrame } from '@protocol/channels';
|
||||
import { serializeWorkerError } from './util';
|
||||
import { testInfoError } from './util';
|
||||
|
||||
export interface TestStepInternal {
|
||||
complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void;
|
||||
complete(result: { error?: Error | unknown, attachments?: Attachment[], suggestedRebaseline?: string }): void;
|
||||
stepId: string;
|
||||
title: string;
|
||||
category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'attach' | string;
|
||||
|
|
@ -41,7 +41,7 @@ export interface TestStepInternal {
|
|||
endWallTime?: number;
|
||||
apiName?: string;
|
||||
params?: Record<string, any>;
|
||||
error?: TestInfoError;
|
||||
error?: TestInfoErrorImpl;
|
||||
infectParentStepsWithError?: boolean;
|
||||
box?: boolean;
|
||||
isStage?: boolean;
|
||||
|
|
@ -97,14 +97,14 @@ export class TestInfoImpl implements TestInfo {
|
|||
snapshotSuffix: string = '';
|
||||
readonly outputDir: string;
|
||||
readonly snapshotDir: string;
|
||||
errors: TestInfoError[] = [];
|
||||
errors: TestInfoErrorImpl[] = [];
|
||||
readonly _attachmentsPush: (...items: TestInfo['attachments']) => number;
|
||||
|
||||
get error(): TestInfoError | undefined {
|
||||
get error(): TestInfoErrorImpl | undefined {
|
||||
return this.errors[0];
|
||||
}
|
||||
|
||||
set error(e: TestInfoError | undefined) {
|
||||
set error(e: TestInfoErrorImpl | undefined) {
|
||||
if (e === undefined)
|
||||
throw new Error('Cannot assign testInfo.error undefined value!');
|
||||
this.errors[0] = e;
|
||||
|
|
@ -273,7 +273,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
if (result.error) {
|
||||
if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol])
|
||||
(result.error as any)[stepSymbol] = step;
|
||||
const error = serializeWorkerError(result.error);
|
||||
const error = testInfoError(result.error);
|
||||
if (data.boxedStack)
|
||||
error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`;
|
||||
step.error = error;
|
||||
|
|
@ -297,6 +297,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
stepId,
|
||||
wallTime: step.endWallTime,
|
||||
error: step.error,
|
||||
suggestedRebaseline: result.suggestedRebaseline,
|
||||
};
|
||||
this._onStepEnd(payload);
|
||||
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) {
|
||||
if (this.status === 'passed' || this.status === 'skipped')
|
||||
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;
|
||||
if (step && step.boxedStack)
|
||||
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 { ManualPromise, calculateSha1, monotonicTime, createGuid, SerializedFS } from 'playwright-core/lib/utils';
|
||||
import { yauzl, yazl } from 'playwright-core/lib/zipBundle';
|
||||
import type { TestInfo, TestInfoError } from '../../types/test';
|
||||
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 { TestInfoErrorImpl } from '../common/ipc';
|
||||
|
||||
export type Attachment = TestInfo['attachments'][0];
|
||||
export const testTraceEntryName = 'test.trace';
|
||||
|
|
@ -219,7 +219,7 @@ export class TestTracing {
|
|||
this._testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
|
||||
}
|
||||
|
||||
appendForError(error: TestInfoError) {
|
||||
appendForError(error: TestInfoErrorImpl) {
|
||||
const rawStack = error.stack?.split('\n') || [];
|
||||
const stack = rawStack ? filteredStackTrace(rawStack) : [];
|
||||
this._appendTraceEvent({
|
||||
|
|
|
|||
|
|
@ -14,32 +14,13 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { TestError } from '../../types/testReporter';
|
||||
import type { TestInfoError } from '../../types/test';
|
||||
import type { MatcherResult } from '../matchers/matcherHint';
|
||||
import type { TestInfoErrorImpl } from '../common/ipc';
|
||||
import { ExpectError } from '../matchers/matcherHint';
|
||||
import { serializeError } from '../util';
|
||||
|
||||
|
||||
type MatcherResultDetails = Pick<TestError, 'timeout'|'matcherName'|'locator'|'expected'|'received'|'log'>;
|
||||
|
||||
export function serializeWorkerError(error: Error | any): TestInfoError & MatcherResultDetails {
|
||||
return {
|
||||
...serializeError(error),
|
||||
...serializeExpectDetails(error),
|
||||
};
|
||||
export function testInfoError(error: Error | any): TestInfoErrorImpl {
|
||||
const result = serializeError(error);
|
||||
if (error instanceof ExpectError)
|
||||
result.matcherResult = error.matcherResult;
|
||||
return result;
|
||||
}
|
||||
|
||||
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 { 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 { deserializeConfig } from '../common/configLoader';
|
||||
import type { Suite, TestCase } from '../common/test';
|
||||
|
|
@ -28,11 +29,10 @@ import { ProcessRunner } from '../common/process';
|
|||
import { loadTestFile } from '../common/testLoader';
|
||||
import { applyRepeatEachIndex, bindFileSuiteToProject, filterTestsRemoveEmptySuites } from '../common/suiteUtils';
|
||||
import { PoolBuilder } from '../common/poolBuilder';
|
||||
import type { TestInfoError } from '../../types/test';
|
||||
import type { Location } from '../../types/testReporter';
|
||||
import { inheritFixtureNames } from '../common/fixtures';
|
||||
import { type TimeSlot } from './timeoutManager';
|
||||
import { serializeWorkerError } from './util';
|
||||
import { testInfoError } from './util';
|
||||
|
||||
export class WorkerMain extends ProcessRunner {
|
||||
private _params: WorkerInitParams;
|
||||
|
|
@ -42,7 +42,7 @@ export class WorkerMain extends ProcessRunner {
|
|||
private _fixtureRunner: FixtureRunner;
|
||||
|
||||
// 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
|
||||
// of a setup error, usually beforeAll hook.
|
||||
private _skipRemainingTestsInSuite: Suite | undefined;
|
||||
|
|
@ -113,7 +113,7 @@ export class WorkerMain extends ProcessRunner {
|
|||
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {});
|
||||
this._fatalErrors.push(...fakeTestInfo.errors);
|
||||
} catch (e) {
|
||||
this._fatalErrors.push(serializeWorkerError(e));
|
||||
this._fatalErrors.push(testInfoError(e));
|
||||
}
|
||||
|
||||
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)
|
||||
return;
|
||||
const count = this._totalRunningTests === 1 ? '1 test' : `${this._totalRunningTests} tests`;
|
||||
|
|
@ -154,7 +154,7 @@ export class WorkerMain extends ProcessRunner {
|
|||
// No current test - fatal error.
|
||||
if (!this._currentTest) {
|
||||
if (!this._fatalErrors.length)
|
||||
this._fatalErrors.push(serializeWorkerError(error));
|
||||
this._fatalErrors.push(testInfoError(error));
|
||||
void this._stop();
|
||||
return;
|
||||
}
|
||||
|
|
@ -225,7 +225,7 @@ export class WorkerMain extends ProcessRunner {
|
|||
// In theory, we should run above code without any errors.
|
||||
// However, in the case we screwed up, or loadTestFile failed in the worker
|
||||
// but not in the runner, let's do a fatal error.
|
||||
this._fatalErrors.push(serializeWorkerError(e));
|
||||
this._fatalErrors.push(testInfoError(e));
|
||||
void this._stop();
|
||||
} finally {
|
||||
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.
|
||||
* - `'on'`: Capture screenshot after each test.
|
||||
* - `'only-on-failure'`: Capture screenshot after each test failure.
|
||||
* - `'on-first-failure'`: Capture screenshot after each test's first failure.
|
||||
*
|
||||
* **Usage**
|
||||
*
|
||||
|
|
@ -5938,7 +5939,7 @@ export interface PlaywrightWorkerOptions {
|
|||
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 VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry';
|
||||
|
||||
|
|
|
|||
|
|
@ -1777,6 +1777,7 @@ export type BrowserContextEnableRecorderParams = {
|
|||
device?: string,
|
||||
saveStorage?: string,
|
||||
outputFile?: string,
|
||||
handleSIGINT?: boolean,
|
||||
omitCallTracking?: boolean,
|
||||
};
|
||||
export type BrowserContextEnableRecorderOptions = {
|
||||
|
|
@ -1790,6 +1791,7 @@ export type BrowserContextEnableRecorderOptions = {
|
|||
device?: string,
|
||||
saveStorage?: string,
|
||||
outputFile?: string,
|
||||
handleSIGINT?: boolean,
|
||||
omitCallTracking?: boolean,
|
||||
};
|
||||
export type BrowserContextEnableRecorderResult = void;
|
||||
|
|
|
|||
|
|
@ -1208,6 +1208,7 @@ BrowserContext:
|
|||
device: string?
|
||||
saveStorage: string?
|
||||
outputFile: string?
|
||||
handleSIGINT: boolean?
|
||||
omitCallTracking: boolean?
|
||||
|
||||
newCDPSession:
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export type BrowserTestWorkerFixtures = PageWorkerFixtures & {
|
|||
browserType: BrowserType;
|
||||
isAndroid: boolean;
|
||||
isElectron: boolean;
|
||||
nodeVersion: { major: number, minor: number, patch: number };
|
||||
bidiTestSkipPredicate: (info: TestInfo) => boolean;
|
||||
};
|
||||
|
||||
|
|
@ -96,6 +97,11 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
|
|||
await run(Number(browserVersion.split('.')[0]));
|
||||
}, { 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' }],
|
||||
isElectron: [false, { 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`);
|
||||
});
|
||||
|
||||
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.skip(+process.versions.node.split('.')[0] < 18);
|
||||
it.skip(nodeVersion.major < 18);
|
||||
|
||||
const encodedRequestPayload = zlib.brotliCompressSync(Buffer.from('A'));
|
||||
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);
|
||||
});
|
||||
|
||||
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' });
|
||||
const nodeVersion = +process.versions.node.split('.')[0];
|
||||
it.skip(nodeVersion < 20, 'File is not available in Node.js < 20. FormData is not available in Node.js < 18');
|
||||
it.skip(nodeVersion.major < 20, 'File is not available in Node.js < 20. FormData is not available in Node.js < 18');
|
||||
const postBodyPromise = new Promise<string>(resolve => {
|
||||
server.setRoute('/empty.html', async (req, res) => {
|
||||
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`);
|
||||
});
|
||||
|
||||
it('should handle malformed file', async ({ contextFactory }, testInfo) => {
|
||||
it('should handle malformed file', async ({ contextFactory, nodeVersion }, testInfo) => {
|
||||
const file = testInfo.outputPath('state.json');
|
||||
fs.writeFileSync(file, 'not-json', 'utf-8');
|
||||
const error = await contextFactory({
|
||||
storageState: file,
|
||||
}).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`);
|
||||
else
|
||||
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();
|
||||
});
|
||||
|
||||
it('should allow await using', async ({ browserType }) => {
|
||||
const nodeVersion = +process.versions.node.split('.')[0];
|
||||
it.skip(nodeVersion < 18);
|
||||
it('should allow await using', async ({ browserType, nodeVersion }) => {
|
||||
it.skip(nodeVersion.major < 18);
|
||||
|
||||
let b: Browser;
|
||||
let c: BrowserContext;
|
||||
|
|
|
|||
|
|
@ -401,6 +401,7 @@ it('should be able to render avif images', {
|
|||
}, async ({ page, server, browserName, platform }) => {
|
||||
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('debian11'), 'Debian 11 is too old');
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`<img src="${server.PREFIX}/rgb.avif" onerror="window.error = true">`);
|
||||
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();
|
||||
});
|
||||
|
||||
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 extensionPath = asset('simple-extension');
|
||||
const extensionOptions = {
|
||||
|
|
@ -75,7 +76,8 @@ it('should return background pages', async ({ browserType, createUserDataDir, as
|
|||
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 extensionPath = asset('simple-extension');
|
||||
const extensionOptions = {
|
||||
|
|
@ -99,7 +101,8 @@ it('should return background pages when recording video', async ({ browserType,
|
|||
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) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html', 'x-response-foobar': 'BarFoo' });
|
||||
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', {
|
||||
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 extensionPath = asset('extension-with-logging');
|
||||
const extensionOptions = {
|
||||
|
|
|
|||
|
|
@ -727,9 +727,9 @@ test.describe('browser', () => {
|
|||
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(+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 page = await browser.newPage({
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { PNG } from 'playwright-core/lib/utilsBundle';
|
|||
import { expect, playwrightTest as it } from '../config/browserTest';
|
||||
|
||||
it.use({ headless: false });
|
||||
it.skip(({ channel }) => channel === 'chromium-headless-shell');
|
||||
|
||||
it('should have default url when launching browser @smoke', async ({ launchPersistent }) => {
|
||||
const { context } = await launchPersistent();
|
||||
|
|
|
|||
|
|
@ -41,9 +41,10 @@ it('should kill browser process on timeout after close', async ({ browserType, m
|
|||
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(mode.startsWith('service'));
|
||||
it.skip(channel === 'chromium-headless-shell', 'Headless Shell is always headless');
|
||||
|
||||
const error: Error = await browserType.launch({
|
||||
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('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 }) => {
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
|
||||
const counts = async () => {
|
||||
const aliveCount = async () => {
|
||||
return await traceViewer.page.evaluate(() => {
|
||||
const weakSet = (window as any)._weakRecordersForTest || new Set();
|
||||
const weakList = [...weakSet];
|
||||
const aliveList = weakList.filter(r => !!r.deref());
|
||||
return { total: weakList.length, alive: aliveList.length };
|
||||
return aliveList.length;
|
||||
});
|
||||
};
|
||||
|
||||
await traceViewer.snapshotFrame('page.goto');
|
||||
await traceViewer.snapshotFrame('page.evaluate');
|
||||
await traceViewer.page.requestGC();
|
||||
await expect.poll(() => counts()).toEqual({ total: 4, alive: 1 });
|
||||
await expect(traceViewer.snapshotContainer.contentFrame().locator('body')).toContainText(`Hi, I'm frame`);
|
||||
|
||||
const frame1 = await traceViewer.snapshotFrame('page.goto');
|
||||
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 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 }) => {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
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 { 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';
|
||||
|
||||
const createClock = (now?: number): ClockController & ClockMethods => {
|
||||
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 { 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', () => {
|
||||
const input = [];
|
||||
|
|
@ -385,3 +385,17 @@ it('should include pseudo codepoints', async ({ page, server }) => {
|
|||
- 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');
|
||||
const screenshot = await page.screenshot();
|
||||
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');
|
||||
else
|
||||
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(`
|
||||
- list "my list":
|
||||
- listitem: one
|
||||
- listitem: two
|
||||
- listitem: "one"
|
||||
- listitem: "two"
|
||||
`);
|
||||
});
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ test('should allow text nodes', async ({ page }) => {
|
|||
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot(`
|
||||
- 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(`
|
||||
- group: Summary
|
||||
- group: "Summary"
|
||||
`);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -511,13 +511,13 @@ test('should support toHaveURL with baseURL from webServer', async ({ runInlineT
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('pass', async ({ page }) => {
|
||||
await page.goto('/foobar');
|
||||
await expect(page).toHaveURL('/foobar');
|
||||
await expect(page).toHaveURL('http://localhost:${port}/foobar');
|
||||
await page.goto('/hello');
|
||||
await expect(page).toHaveURL('/hello');
|
||||
await expect(page).toHaveURL('http://localhost:${port}/hello');
|
||||
});
|
||||
|
||||
test('fail', async ({ page }) => {
|
||||
await page.goto('/foobar');
|
||||
await page.goto('/hello');
|
||||
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) => {
|
||||
const result = await runInlineTest({
|
||||
'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) => {
|
||||
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({
|
||||
'playwright.config.ts': `
|
||||
module.exports = { use: { trace: 'on', screenshot: 'on' } };
|
||||
`,
|
||||
'a.spec.ts': `
|
||||
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.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');
|
||||
await page.click('a');
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBe('empty.pdf');
|
||||
expect(download.suggestedFilename()).toBe('file.txt');
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
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"`);
|
||||
// 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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -65,19 +65,19 @@ test('should run visible', async ({ runUITest }) => {
|
|||
- tree:
|
||||
- treeitem "[icon-error] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||
- treeitem ${/\[icon-error\] fails \d+ms/} [selected]:
|
||||
- treeitem ${/\[icon-check\] passes/}
|
||||
- treeitem ${/\[icon-error\] fails/} [selected]:
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
- treeitem "[icon-error] suite"
|
||||
- treeitem "[icon-error] b.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||
- treeitem ${/\[icon-error\] fails \d+ms/}
|
||||
- treeitem ${/\[icon-check\] passes/}
|
||||
- treeitem ${/\[icon-error\] fails/}
|
||||
- treeitem "[icon-check] c.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||
- treeitem ${/\[icon-check\] passes/}
|
||||
- treeitem "[icon-circle-slash] skipped"
|
||||
`);
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ test('should run on hover', async ({ runUITest }) => {
|
|||
- tree:
|
||||
- treeitem "[icon-circle-outline] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] passes \d+ms/}:
|
||||
- treeitem ${/\[icon-check\] passes/}:
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
|
|
@ -185,7 +185,7 @@ test('should run on Enter', async ({ runUITest }) => {
|
|||
- treeitem "[icon-error] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem "[icon-circle-outline] passes"
|
||||
- treeitem ${/\[icon-error\] fails \d+ms/} [selected]:
|
||||
- treeitem ${/\[icon-error\] fails/} [selected]:
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
|
|
@ -225,19 +225,19 @@ test('should run by project', async ({ runUITest }) => {
|
|||
- tree:
|
||||
- treeitem "[icon-error] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||
- treeitem ${/\[icon-error\] fails \d+ms/} [selected]:
|
||||
- treeitem ${/\[icon-check\] passes/}
|
||||
- treeitem ${/\[icon-error\] fails/} [selected]:
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
- treeitem "[icon-error] suite"
|
||||
- treeitem "[icon-error] b.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||
- treeitem ${/\[icon-error\] fails \d+ms/}
|
||||
- treeitem ${/\[icon-check\] passes/}
|
||||
- treeitem ${/\[icon-error\] fails/}
|
||||
- treeitem "[icon-check] c.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||
- treeitem ${/\[icon-check\] passes/}
|
||||
- treeitem "[icon-circle-slash] skipped"
|
||||
`);
|
||||
|
||||
|
|
@ -299,14 +299,14 @@ test('should run by project', async ({ runUITest }) => {
|
|||
- tree:
|
||||
- treeitem "[icon-error] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-circle-outline\] passes \d+ms/} [expanded] [selected]:
|
||||
- treeitem ${/\[icon-circle-outline\] passes/} [expanded] [selected]:
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] foo \d+ms/}
|
||||
- treeitem ${/\[icon-check\] foo/}
|
||||
- treeitem ${/\[icon-circle-outline\] bar/}
|
||||
- treeitem ${/\[icon-error\] fails \d+ms/}
|
||||
- treeitem ${/\[icon-error\] fails/}
|
||||
`);
|
||||
|
||||
await expect(page.getByText('Projects: foo bar')).toBeVisible();
|
||||
|
|
@ -333,17 +333,17 @@ test('should run by project', async ({ runUITest }) => {
|
|||
- tree:
|
||||
- treeitem "[icon-error] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] passes \d+ms/} [expanded]:
|
||||
- treeitem ${/\[icon-check\] passes/} [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] foo \d+ms/}
|
||||
- treeitem ${/\[icon-check\] bar \d+ms/}
|
||||
- treeitem ${/\[icon-error\] fails \d+ms/} [expanded]:
|
||||
- treeitem ${/\[icon-check\] foo/}
|
||||
- treeitem ${/\[icon-check\] bar/}
|
||||
- treeitem ${/\[icon-error\] fails/} [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-error\] foo \d+ms/} [selected]:
|
||||
- treeitem ${/\[icon-error\] foo/} [selected]:
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
- treeitem ${/\[icon-error\] bar \d+ms/}
|
||||
- treeitem ${/\[icon-error\] bar/}
|
||||
- treeitem ${/\[icon-error\] suite/}
|
||||
- treeitem "[icon-error] b.test.ts" [expanded]:
|
||||
- group:
|
||||
|
|
@ -385,7 +385,7 @@ test('should stop', async ({ runUITest }) => {
|
|||
- treeitem "[icon-loading] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- 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-clock\] test 3/}
|
||||
`);
|
||||
|
|
@ -408,7 +408,7 @@ test('should stop', async ({ runUITest }) => {
|
|||
- treeitem "[icon-circle-outline] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- 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 3/}
|
||||
`);
|
||||
|
|
@ -478,19 +478,19 @@ test('should show time', async ({ runUITest }) => {
|
|||
- tree:
|
||||
- treeitem "[icon-error] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||
- treeitem ${/\[icon-error\] fails \d+ms/} [selected]:
|
||||
- treeitem ${/\[icon-check\] passes \d+m?s/}
|
||||
- treeitem ${/\[icon-error\] fails \d+m?s/} [selected]:
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
- treeitem "[icon-error] suite"
|
||||
- treeitem "[icon-error] b.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||
- treeitem ${/\[icon-error\] fails \d+ms/}
|
||||
- treeitem ${/\[icon-check\] passes \d+m?s/}
|
||||
- treeitem ${/\[icon-error\] fails \d+m?s/}
|
||||
- treeitem "[icon-check] c.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||
- treeitem ${/\[icon-check\] passes \d+m?s/}
|
||||
- treeitem "[icon-circle-slash] skipped"
|
||||
`);
|
||||
|
||||
|
|
@ -522,7 +522,7 @@ test('should show test.fail as passing', async ({ runUITest }) => {
|
|||
- tree:
|
||||
- treeitem "[icon-check] a.test.ts" [expanded]:
|
||||
- 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%)');
|
||||
|
|
@ -558,7 +558,7 @@ test('should ignore repeatEach', async ({ runUITest }) => {
|
|||
- tree:
|
||||
- treeitem "[icon-check] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] should pass \d+ms/}
|
||||
- treeitem ${/\[icon-check\] should pass/}
|
||||
`);
|
||||
|
||||
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:
|
||||
- treeitem "[icon-check] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] should pass \d+ms/}
|
||||
- treeitem ${/\[icon-check\] should pass/}
|
||||
`);
|
||||
|
||||
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:
|
||||
- treeitem "[icon-check] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] should pass \d+ms/}
|
||||
- treeitem ${/\[icon-check\] should pass/}
|
||||
`);
|
||||
|
||||
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:
|
||||
- treeitem "[icon-circle-outline] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] run @setup setup \d+ms/} [selected]:
|
||||
- treeitem ${/\[icon-check\] run @setup setup/} [selected]:
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
|
|
@ -676,8 +676,8 @@ test('should show proper total when using deps', async ({ runUITest }) => {
|
|||
- tree:
|
||||
- treeitem "[icon-check] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] run @setup setup \d+ms/}
|
||||
- treeitem ${/\[icon-check\] run @chromium chromium \d+ms/} [selected]:
|
||||
- treeitem ${/\[icon-check\] run @setup setup/}
|
||||
- treeitem ${/\[icon-check\] run @chromium chromium/} [selected]:
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
|
|
@ -746,7 +746,7 @@ test('should respect --tsconfig option', {
|
|||
- tree:
|
||||
- treeitem "[icon-check] a.test.ts" [expanded]:
|
||||
- group:
|
||||
- treeitem ${/\[icon-check\] test \d+ms/}
|
||||
- treeitem ${/\[icon-check\] test/}
|
||||
`);
|
||||
|
||||
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||
|
|
@ -775,6 +775,6 @@ test('should respect --ignore-snapshots option', {
|
|||
- tree:
|
||||
- treeitem "[icon-check] a.test.ts" [expanded]:
|
||||
- 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': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('should work', async ({ page }) => {
|
||||
await page.goto('http://localhost:${port}');
|
||||
await page.goto('http://localhost:${port}/hello');
|
||||
});
|
||||
`
|
||||
}, { 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 };
|
||||
}
|
||||
|
||||
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 VideoMode = 'off' | 'on' | 'retain-on-failure' | 'on-first-retry';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue