Compare commits

...

12 commits

Author SHA1 Message Date
Playwright Service 8aef8e8fc2
cherry-pick(#28197): fix(chromium): properly detect session closed errors for oopifs (#28508)
This PR cherry-picks the following commits:

- da6707f785

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-06 13:53:00 -08:00
Dmitry Gozman 268942037a
chore: mark 1.40.1 (#28380) 2023-11-28 10:48:24 -08:00
Yury Semikhatsky efc7ec1e80
cherry-pick(#28366): fix: parse report.jsonl without creating large s… (#28378)
…tring

Fixes https://github.com/microsoft/playwright/issues/28362
2023-11-28 10:04:15 -08:00
Dmitry Gozman 43798aff92
cherry-pick(#28360): Revert "chore(test runner): remove fake skipped test results (#27762)" (#28368)
This reverts commit 210168e36d.

Fixes #28321.
2023-11-27 19:01:48 -08:00
Pavel Feldman 18478e325f cherry-pick(#28365): chore: do not add to the internal action logs 2023-11-27 16:45:22 -08:00
Playwright Service d19c948ce7
cherry-pick(#28302): docs: Update codegen documentation and screenshots (#28311) 2023-11-23 14:20:54 +01:00
Playwright Service 480ae58942
cherry-pick(#28276): docs(trace-viewer): fix <details><summary> syntax (#28297)
This PR cherry-picks the following commits:

- e405c1deea

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2023-11-22 19:55:59 +01:00
Playwright Service 8dea604754
cherry-pick(#28267): docs(python): add ignoreCase to NotToHaveAttribute (#28273)
This PR cherry-picks the following commits:

- d7d1c80cf7

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2023-11-21 18:20:09 +01:00
Playwright Service 65859fa54b
cherry-pick(#28271): docs(release-notes): 1.40 nits (#28274)
This PR cherry-picks the following commits:

- 8c880cad76

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2023-11-21 18:20:01 +01:00
Playwright Service e27bc9faa8
cherry-pick(#28239): fix: collect all errors in removeFolders (#28243)
This PR cherry-picks the following commits:

- 440f5e5d2b

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2023-11-20 21:48:34 +01:00
Dmitry Gozman b8949166dc
cherry-pick(#28198): feat(recorder): UX updates for assertion tools (#28202)
- No locator editor.
- No value editor for `toHaveValue`.
- Visual feedback for `toBeVisible`/`toHaveValue`.
- UI tweaks.
2023-11-16 13:30:01 -08:00
Dmitry Gozman 59e8f4815d
chore: mark v1.40.0 (#28199) 2023-11-16 12:31:06 -08:00
45 changed files with 394 additions and 397 deletions

View file

@ -244,6 +244,12 @@ Attribute name.
Expected attribute value. Expected attribute value.
### option: LocatorAssertions.NotToHaveAttribute.ignoreCase
* since: v1.40
- `ignoreCase` <[boolean]>
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.
### option: LocatorAssertions.NotToHaveAttribute.timeout = %%-csharp-java-python-assertions-timeout-%% ### option: LocatorAssertions.NotToHaveAttribute.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.18 * since: v1.18

View file

@ -36,25 +36,33 @@ pwsh bin/Debug/netX/playwright.ps1 codegen demo.playwright.dev/todomvc
Run `codegen` and perform actions in the browser. Playwright will generate the code for the user interactions. `Codegen` will look at the rendered page and figure out the recommended locator, prioritizing role, text and test id locators. If the generator identifies multiple elements matching the locator, it will improve the locator to make it resilient and uniquely identify the target element, therefore eliminating and reducing test(s) failing and flaking due to locators. Run `codegen` and perform actions in the browser. Playwright will generate the code for the user interactions. `Codegen` will look at the rendered page and figure out the recommended locator, prioritizing role, text and test id locators. If the generator identifies multiple elements matching the locator, it will improve the locator to make it resilient and uniquely identify the target element, therefore eliminating and reducing test(s) failing and flaking due to locators.
With the test generator you can record:
* Actions like click or fill by simply interacting with the page
* Assertions by clicking on one of the icons in the toolbar and then clicking on an element on the page to assert against. You can choose:
* `'assert visibility'` to assert that an element is visible
* `'assert text'` to assert that an element contains specific text
* `'assert value'` to assert that an element has a specific value
###### ######
* langs: js * langs: js
![Recording a test](https://github.com/microsoft/playwright/assets/13063165/9effe72a-3bfd-42e1-87f3-2e6b0a2b71f9) ![Recording a test](https://github.com/microsoft/playwright/assets/13063165/34a79ea1-639e-4cb3-8115-bfdc78e3d34d)
###### ######
* langs: java * langs: java
![recording a test](https://github.com/microsoft/playwright/assets/13063165/26183fc4-a8a1-4d1c-9cdc-aca404a6eb9c) ![recording a test](https://github.com/microsoft/playwright/assets/13063165/ec9c4071-4af8-4ae7-8b36-aebcc29bdbbb)
###### ######
* langs: python * langs: python
![recording a test](https://github.com/microsoft/playwright/assets/13063165/57ed3f29-6436-4f2b-98ad-05de92d30075) ![recording a test](https://github.com/microsoft/playwright/assets/13063165/9751b609-6e4c-486b-a961-f86f177b1d58)
###### ######
* langs: csharp * langs: csharp
![recording a test](https://github.com/microsoft/playwright/assets/13063165/06bd474b-cdd1-4384-9de2-c745f296c78c) ![recording a test](https://github.com/microsoft/playwright/assets/13063165/53bdfb6f-d462-4ce0-ab95-0619faaebf1e)
###### ######
* langs: js, java, python, csharp * langs: js, java, python, csharp
@ -77,22 +85,22 @@ You can generate [locators](/locators.md) with the test generator.
###### ######
* langs: js * langs: js
![picking a locator](https://github.com/microsoft/playwright/assets/13063165/4e46e1dd-dac2-4372-b643-00f896bb7e5f) ![picking a locator](https://github.com/microsoft/playwright/assets/13063165/2c8a12e2-4e98-4fdd-af92-1d73ae696d86)
###### ######
* langs: java * langs: java
![picking a locator](https://github.com/microsoft/playwright/assets/13063165/6200e6d1-e420-422c-9b62-831ec3fd43ea) ![picking a locator](https://github.com/microsoft/playwright/assets/13063165/733b48fd-5edf-4150-93f0-018adc52b6ff)
###### ######
* langs: python * langs: python
![picking a locator](https://github.com/microsoft/playwright/assets/13063165/49ad6214-dfec-4aae-b86c-0fdf05278293) ![picking a locator](https://github.com/microsoft/playwright/assets/13063165/95d11f48-96a4-46b9-9c2a-63c3aa4fdce7)
###### ######
* langs: csharp * langs: csharp
![picking a locator](https://github.com/microsoft/playwright/assets/13063165/d8d47fbc-38d6-4a6b-a9ab-4c40380f480b) ![picking a locator](https://github.com/microsoft/playwright/assets/13063165/1478f56f-422f-4276-9696-0674041f11dc)
### Emulation ### Emulation

View file

@ -27,17 +27,25 @@ To record a test click on the **Record new** button from the Testing sidebar. Th
In the browser go to the URL you wish to test and start clicking around to record your user actions. In the browser go to the URL you wish to test and start clicking around to record your user actions.
<img width="1394" alt="clicking delete button on todo app with locator highlighted" src="https://user-images.githubusercontent.com/13063165/220957132-31b54f82-6235-4c52-a966-6863553b5b23.png" /> ![generating user actions](https://github.com/microsoft/playwright/assets/13063165/1d4c8f37-8325-4816-a665-d0e95e63f509)
Playwright will record your actions and generate the test code directly in VS Code. Once you are done recording click the **cancel** button or close the browser window. You can then inspect your `test-1.spec.ts` file and see your generated test and then manually improve the test by adding [ assertions](test-assertions). Playwright will record your actions and generate the test code directly in VS Code. You can also generate assertions by choosing one of the icons in the toolbar and then clicking on an element on the page to assert against. The following assertions can be generated:
* `'assert visibility'` to assert that an element is visible
* `'assert text'` to assert that an element contains specific text
* `'assert value'` to assert that an element has a specific value
<img width="1667" alt="vs code showing recorded actions of test" src="https://user-images.githubusercontent.com/13063165/220938674-6e1ff1d3-e75a-4238-a7fc-4c40dbc8b3bc.png" /> ![generating assertions](https://github.com/microsoft/playwright/assets/13063165/d131eb35-b2ca-4bf4-a8ac-88b6e40dcf07)
Once you are done recording click the **cancel** button or close the browser window. You can then inspect your `test-1.spec.ts` file and manually improve it if needed.
![code from a generated test](https://github.com/microsoft/playwright/assets/13063165/2ba4c212-4713-460a-b054-6dc6b67a9a7c)
### Record at Cursor ### Record at Cursor
To record from a specific point in your test move your cursor to where you want to record more actions and then click the **Record at cursor** button from the Testing sidebar. If your browser window is not already open then first run the test with 'Show browser' checked and then click the **Record at cursor** button. To record from a specific point in your test move your cursor to where you want to record more actions and then click the **Record at cursor** button from the Testing sidebar. If your browser window is not already open then first run the test with 'Show browser' checked and then click the **Record at cursor** button.
<img width="1529" alt="record at cursor in vs code" src="https://user-images.githubusercontent.com/13063165/220959996-2bb3af59-85d9-4d58-aba7-d57375e7ca7e.png" />
![record at cursor in vs code](https://github.com/microsoft/playwright/assets/13063165/77948ab8-92a2-435f-9833-0944da5ae664)
In the browser window start performing the actions you want to record. In the browser window start performing the actions you want to record.
@ -46,7 +54,7 @@ In the browser window start performing the actions you want to record.
In the test file in VS Code you will see your new generated actions added to your test at the cursor position. In the test file in VS Code you will see your new generated actions added to your test at the cursor position.
<img width="1641" alt="vs code showing test code generated" src="https://user-images.githubusercontent.com/13063165/220940902-d1dbc321-0ef5-4388-9e11-6311aff59ff4.png" /> ![code from a generated test](https://github.com/microsoft/playwright/assets/13063165/4f4bb34e-9cda-41fe-bf65-8d8016d84c7f)
### Generating locators ### Generating locators
@ -85,25 +93,35 @@ pwsh bin/Debug/netX/playwright.ps1 codegen demo.playwright.dev/todomvc
Run the `codegen` command and perform actions in the browser window. Playwright will generate the code for the user interactions which you can see in the Playwright Inspector window. Once you have finished recording your test stop the recording and press the **copy** button to copy your generated test into your editor. Run the `codegen` command and perform actions in the browser window. Playwright will generate the code for the user interactions which you can see in the Playwright Inspector window. Once you have finished recording your test stop the recording and press the **copy** button to copy your generated test into your editor.
With the test generator you can record:
* Actions like click or fill by simply interacting with the page
* Assertions by clicking on one of the icons in the toolbar and then clicking on an element on the page to assert against. You can choose:
* `'assert visibility'` to assert that an element is visible
* `'assert text'` to assert that an element contains specific text
* `'assert value'` to assert that an element has a specific value
###### ######
* langs: js * langs: js
<img width="1365" alt="Recording a test" src="https://user-images.githubusercontent.com/13063165/212754505-b98e80fd-6dda-48f7-860b-b32b4fabee33.png" /> ![Recording a test](https://github.com/microsoft/playwright/assets/13063165/34a79ea1-639e-4cb3-8115-bfdc78e3d34d)
###### ######
* langs: java * langs: java
<img width="1365" alt="Recording a test" src="https://user-images.githubusercontent.com/13063165/212754804-0d9f9d52-0a48-45c8-970d-e672d4a91221.png" /> ![recording a test](https://github.com/microsoft/playwright/assets/13063165/ec9c4071-4af8-4ae7-8b36-aebcc29bdbbb)
###### ######
* langs: python * langs: python
<img width="1365" alt="Recording a test" src="https://user-images.githubusercontent.com/13063165/212751993-b7da2c40-a7cc-4b13-9a91-40ee837042a1.png" /> ![recording a test](https://github.com/microsoft/playwright/assets/13063165/9751b609-6e4c-486b-a961-f86f177b1d58)
###### ######
* langs: csharp * langs: csharp
<img width="1365" alt="Recording a test" src="https://user-images.githubusercontent.com/13063165/212754994-fa637d81-b81d-44b8-bcd7-5dc218034f0a.png" /> ![recording a test](https://github.com/microsoft/playwright/assets/13063165/53bdfb6f-d462-4ce0-ab95-0619faaebf1e)
######
* langs: js, java, python, csharp
When you have finished interacting with the page, press the **record** button to stop the recording and use the **copy** button to copy the generated code to your editor. When you have finished interacting with the page, press the **record** button to stop the recording and use the **copy** button to copy the generated code to your editor.
@ -122,22 +140,22 @@ You can generate [locators](/locators.md) with the test generator.
###### ######
* langs: js * langs: js
<img width="1321" alt="Picking a locator" src="https://user-images.githubusercontent.com/13063165/212753129-55fbcf69-0be3-422e-888a-f52060c7aa6b.png" /> ![picking a locator](https://github.com/microsoft/playwright/assets/13063165/2c8a12e2-4e98-4fdd-af92-1d73ae696d86)
###### ######
* langs: java * langs: java
<img width="1321" alt="Picking a locator" src="https://user-images.githubusercontent.com/13063165/212753446-456484a8-8c37-4104-8db5-4525b74c8cf1.png" /> ![picking a locator](https://github.com/microsoft/playwright/assets/13063165/733b48fd-5edf-4150-93f0-018adc52b6ff)
###### ######
* langs: python * langs: python
<img width="1321" alt="Picking a locator" src="https://user-images.githubusercontent.com/13063165/212753605-861d66a4-fc1c-4559-b821-cb1f39059337.png" /> ![picking a locator](https://github.com/microsoft/playwright/assets/13063165/95d11f48-96a4-46b9-9c2a-63c3aa4fdce7)
###### ######
* langs: csharp * langs: csharp
<img width="1321" alt="Picking a locator" src="https://user-images.githubusercontent.com/13063165/212753728-49d35a7c-c05a-4298-bf66-89930d2cb578.png" /> ![picking a locator](https://github.com/microsoft/playwright/assets/13063165/1478f56f-422f-4276-9696-0674041f11dc)
## Emulation ## Emulation

View file

@ -145,23 +145,28 @@ CodeGen will auto generate your tests for you as you perform actions in the brow
### Record a New Test ### Record a New Test
To record a test click on the **Record new** button from the Testing sidebar. This will create a `test-1.spec.ts` file as well as open up a browser window. In the browser go to the URL you wish to test and start clicking around. Playwright will record your actions and generate a test for you. Once you are done recording click the **cancel** button or close the browser window. You can then inspect your `test-1.spec.ts` file and see your generated test. To record a test click on the **Record new** button from the Testing sidebar. This will create a `test-1.spec.ts` file as well as open up a browser window. In the browser go to the URL you wish to test and start clicking around. Playwright will record your actions and generate the test code directly in VS Code. You can also generate assertions by choosing one of the icons in the toolbar and then clicking on an element on the page to assert against. The following assertions can be generated:
* `'assert visibility'` to assert that an element is visible
* `'assert text'` to assert that an element contains specific text
* `'assert value'` to assert that an element has a specific value
Once you are done recording click the **cancel** button or close the browser window. You can then inspect your `test-1.spec.ts` file and see your generated test.
![record a new test](https://github.com/microsoft/playwright/assets/13063165/a81eb147-e479-4911-82b0-28fb47823c44) ![record a new test](https://github.com/microsoft/playwright/assets/13063165/0407f112-e1cd-41e7-a05d-ae64e24d27ed)
### Record at Cursor ### Record at Cursor
To record from a specific point in your test file click the **Record at cursor** button from the Testing sidebar. This generates actions into the existing test at the current cursor position. You can run the test, position the cursor at the end of the test and continue generating the test. To record from a specific point in your test file click the **Record at cursor** button from the Testing sidebar. This generates actions into the existing test at the current cursor position. You can run the test, position the cursor at the end of the test and continue generating the test.
![record at cursor](https://github.com/microsoft/playwright/assets/13063165/a636d95f-6e72-4d02-9f9f-60e161089e99) ![record at cursor](https://github.com/microsoft/playwright/assets/13063165/96933ea1-4c84-453a-acd7-22b4d3bde185)
### Picking a Locator ### Picking a Locator
Pick a [locator](./locators.md) and copy it into your test file by clicking the **Pick locator** button form the testing sidebar. Then in the browser click the element you require and it will now show up in the **Pick locator** box in VS Code. Press 'enter' on your keyboard to copy the locator into the clipboard and then paste anywhere in your code. Or press 'escape' if you want to cancel. Pick a [locator](./locators.md) and copy it into your test file by clicking the **Pick locator** button form the testing sidebar. Then in the browser click the element you require and it will now show up in the **Pick locator** box in VS Code. Press 'enter' on your keyboard to copy the locator into the clipboard and then paste anywhere in your code. Or press 'escape' if you want to cancel.
![pick locators](https://github.com/microsoft/playwright/assets/13063165/dcb724a6-deb7-4993-b04a-3030cb76a22d) ![pick locators](https://github.com/microsoft/playwright/assets/13063165/9a1b2da9-9ac7-4def-a9e0-f94770364fc2)
Playwright will look at your page and figure out the best locator, prioritizing [role, text and test id locators](./locators.md). If the generator finds multiple elements matching the locator, it will improve the locator to make it resilient and uniquely identify the target element, so you don't have to worry about failing tests due to locators. Playwright will look at your page and figure out the best locator, prioritizing [role, text and test id locators](./locators.md). If the generator finds multiple elements matching the locator, it will improve the locator to make it resilient and uniquely identify the target element, so you don't have to worry about failing tests due to locators.

View file

@ -8,7 +8,7 @@ toc_max_heading_level: 2
### Test Generator Update ### Test Generator Update
![Playwright Test Generator](https://github.com/microsoft/playwright/assets/9881434/8c3d6fac-5381-4aaf-920f-6e22b964eec6) ![Playwright Test Generator](https://github.com/microsoft/playwright/assets/9881434/e8d67e2e-f36d-4301-8631-023948d3e190)
New tools to generate assertions: New tools to generate assertions:
- "Assert visibility" tool generates [`method: LocatorAssertions.toBeVisible`]. - "Assert visibility" tool generates [`method: LocatorAssertions.toBeVisible`].
@ -35,7 +35,7 @@ await Expect(Page.GetByPlaceholder("Search docs")).ToHaveValueAsync("locator");
### Other Changes ### Other Changes
- Methods [`method: Download.path`] and [`method: Download.createReadStream`] throw an error for failed and cancelled downloads. - Methods [`method: Download.path`] and [`method: Download.createReadStream`] throw an error for failed and cancelled downloads.
- Playwright [docker image](./docker.md) now comes with Node.js v20. - Playwright [docker image](./docker.md) now comes with .NET 8 (new LTS).
### Browser Versions ### Browser Versions

View file

@ -8,7 +8,7 @@ toc_max_heading_level: 2
### Test Generator Update ### Test Generator Update
![Playwright Test Generator](https://github.com/microsoft/playwright/assets/9881434/8c3d6fac-5381-4aaf-920f-6e22b964eec6) ![Playwright Test Generator](https://github.com/microsoft/playwright/assets/9881434/e8d67e2e-f36d-4301-8631-023948d3e190)
New tools to generate assertions: New tools to generate assertions:
- "Assert visibility" tool generates [`method: LocatorAssertions.toBeVisible`]. - "Assert visibility" tool generates [`method: LocatorAssertions.toBeVisible`].
@ -35,7 +35,6 @@ assertThat(page.getByPlaceholder("Search docs")).hasValue("locator");
### Other Changes ### Other Changes
- Methods [`method: Download.path`] and [`method: Download.createReadStream`] throw an error for failed and cancelled downloads. - Methods [`method: Download.path`] and [`method: Download.createReadStream`] throw an error for failed and cancelled downloads.
- Playwright [docker image](./docker.md) now comes with Node.js v20.
### Browser Versions ### Browser Versions

View file

@ -8,9 +8,14 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
## Version 1.40 ## Version 1.40
<LiteYouTube
id="mn892dV81_8"
title="Playwright 1.40"
/>
### Test Generator Update ### Test Generator Update
![Playwright Test Generator](https://github.com/microsoft/playwright/assets/9881434/8c3d6fac-5381-4aaf-920f-6e22b964eec6) ![Playwright Test Generator](https://github.com/microsoft/playwright/assets/9881434/e8d67e2e-f36d-4301-8631-023948d3e190)
New tools to generate assertions: New tools to generate assertions:
- "Assert visibility" tool generates [`method: LocatorAssertions.toBeVisible`]. - "Assert visibility" tool generates [`method: LocatorAssertions.toBeVisible`].

View file

@ -8,7 +8,7 @@ toc_max_heading_level: 2
### Test Generator Update ### Test Generator Update
![Playwright Test Generator](https://github.com/microsoft/playwright/assets/9881434/8c3d6fac-5381-4aaf-920f-6e22b964eec6) ![Playwright Test Generator](https://github.com/microsoft/playwright/assets/9881434/e8d67e2e-f36d-4301-8631-023948d3e190)
New tools to generate assertions: New tools to generate assertions:
- "Assert visibility" tool generates [`method: LocatorAssertions.toBeVisible`]. - "Assert visibility" tool generates [`method: LocatorAssertions.toBeVisible`].
@ -38,7 +38,6 @@ def test_example(page: Page) -> None:
### Other Changes ### Other Changes
- Method [`method: Download.path`] throws an error for failed and cancelled downloads. - Method [`method: Download.path`] throws an error for failed and cancelled downloads.
- Playwright [docker image](./docker.md) now comes with Node.js v20.
### Browser Versions ### Browser Versions

View file

@ -28,8 +28,8 @@ Options for tracing are:
This will record the trace and place it into the file named `trace.zip` in your `test-results` directory. This will record the trace and place it into the file named `trace.zip` in your `test-results` directory.
<details><summary>If you are not using Pytest, click here to learn how to record traces. <details>
</summary> <summary>If you are not using Pytest, click here to learn how to record traces.</summary>
```python async ```python async
browser = await chromium.launch() browser = await chromium.launch()

View file

@ -177,8 +177,8 @@ Options for tracing are:
This will record the trace and place it into the file named `trace.zip` in your `test-results` directory. This will record the trace and place it into the file named `trace.zip` in your `test-results` directory.
<details><summary>If you are not using Pytest, click here to learn how to record traces. <details>
</summary> <summary>If you are not using Pytest, click here to learn how to record traces.</summary>
```python async ```python async
browser = await chromium.launch() browser = await chromium.launch()

100
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "playwright-internal", "name": "playwright-internal",
"version": "1.40.0-next", "version": "1.40.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "playwright-internal", "name": "playwright-internal",
"version": "1.40.0-next", "version": "1.40.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@ -6976,10 +6976,10 @@
} }
}, },
"packages/playwright": { "packages/playwright": {
"version": "1.40.0-next", "version": "1.40.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -6993,11 +6993,11 @@
}, },
"packages/playwright-browser-chromium": { "packages/playwright-browser-chromium": {
"name": "@playwright/browser-chromium", "name": "@playwright/browser-chromium",
"version": "1.40.0-next", "version": "1.40.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=16"
@ -7005,11 +7005,11 @@
}, },
"packages/playwright-browser-firefox": { "packages/playwright-browser-firefox": {
"name": "@playwright/browser-firefox", "name": "@playwright/browser-firefox",
"version": "1.40.0-next", "version": "1.40.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=16"
@ -7017,22 +7017,22 @@
}, },
"packages/playwright-browser-webkit": { "packages/playwright-browser-webkit": {
"name": "@playwright/browser-webkit", "name": "@playwright/browser-webkit",
"version": "1.40.0-next", "version": "1.40.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=16"
} }
}, },
"packages/playwright-chromium": { "packages/playwright-chromium": {
"version": "1.40.0-next", "version": "1.40.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7042,7 +7042,7 @@
} }
}, },
"packages/playwright-core": { "packages/playwright-core": {
"version": "1.40.0-next", "version": "1.40.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
@ -7053,11 +7053,11 @@
}, },
"packages/playwright-ct-core": { "packages/playwright-ct-core": {
"name": "@playwright/experimental-ct-core", "name": "@playwright/experimental-ct-core",
"version": "1.40.0-next", "version": "1.40.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.40.0-next", "playwright": "1.40.1",
"playwright-core": "1.40.0-next", "playwright-core": "1.40.1",
"vite": "^4.4.10" "vite": "^4.4.10"
}, },
"bin": { "bin": {
@ -7069,10 +7069,10 @@
}, },
"packages/playwright-ct-react": { "packages/playwright-ct-react": {
"name": "@playwright/experimental-ct-react", "name": "@playwright/experimental-ct-react",
"version": "1.40.0-next", "version": "1.40.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@vitejs/plugin-react": "^4.0.0" "@vitejs/plugin-react": "^4.0.0"
}, },
"bin": { "bin": {
@ -7101,10 +7101,10 @@
}, },
"packages/playwright-ct-react17": { "packages/playwright-ct-react17": {
"name": "@playwright/experimental-ct-react17", "name": "@playwright/experimental-ct-react17",
"version": "1.40.0-next", "version": "1.40.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@vitejs/plugin-react": "^4.0.0" "@vitejs/plugin-react": "^4.0.0"
}, },
"bin": { "bin": {
@ -7133,10 +7133,10 @@
}, },
"packages/playwright-ct-solid": { "packages/playwright-ct-solid": {
"name": "@playwright/experimental-ct-solid", "name": "@playwright/experimental-ct-solid",
"version": "1.40.0-next", "version": "1.40.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"vite-plugin-solid": "^2.7.0" "vite-plugin-solid": "^2.7.0"
}, },
"bin": { "bin": {
@ -7151,10 +7151,10 @@
}, },
"packages/playwright-ct-svelte": { "packages/playwright-ct-svelte": {
"name": "@playwright/experimental-ct-svelte", "name": "@playwright/experimental-ct-svelte",
"version": "1.40.0-next", "version": "1.40.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@sveltejs/vite-plugin-svelte": "^2.1.1" "@sveltejs/vite-plugin-svelte": "^2.1.1"
}, },
"bin": { "bin": {
@ -7169,10 +7169,10 @@
}, },
"packages/playwright-ct-vue": { "packages/playwright-ct-vue": {
"name": "@playwright/experimental-ct-vue", "name": "@playwright/experimental-ct-vue",
"version": "1.40.0-next", "version": "1.40.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@vitejs/plugin-vue": "^4.2.1" "@vitejs/plugin-vue": "^4.2.1"
}, },
"bin": { "bin": {
@ -7220,10 +7220,10 @@
}, },
"packages/playwright-ct-vue2": { "packages/playwright-ct-vue2": {
"name": "@playwright/experimental-ct-vue2", "name": "@playwright/experimental-ct-vue2",
"version": "1.40.0-next", "version": "1.40.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@vitejs/plugin-vue2": "^2.2.0" "@vitejs/plugin-vue2": "^2.2.0"
}, },
"bin": { "bin": {
@ -7237,11 +7237,11 @@
} }
}, },
"packages/playwright-firefox": { "packages/playwright-firefox": {
"version": "1.40.0-next", "version": "1.40.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7275,10 +7275,10 @@
}, },
"packages/playwright-test": { "packages/playwright-test": {
"name": "@playwright/test", "name": "@playwright/test",
"version": "1.40.0-next", "version": "1.40.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.40.0-next" "playwright": "1.40.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7288,11 +7288,11 @@
} }
}, },
"packages/playwright-webkit": { "packages/playwright-webkit": {
"version": "1.40.0-next", "version": "1.40.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8032,33 +8032,33 @@
"@playwright/browser-chromium": { "@playwright/browser-chromium": {
"version": "file:packages/playwright-browser-chromium", "version": "file:packages/playwright-browser-chromium",
"requires": { "requires": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
} }
}, },
"@playwright/browser-firefox": { "@playwright/browser-firefox": {
"version": "file:packages/playwright-browser-firefox", "version": "file:packages/playwright-browser-firefox",
"requires": { "requires": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
} }
}, },
"@playwright/browser-webkit": { "@playwright/browser-webkit": {
"version": "file:packages/playwright-browser-webkit", "version": "file:packages/playwright-browser-webkit",
"requires": { "requires": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
} }
}, },
"@playwright/experimental-ct-core": { "@playwright/experimental-ct-core": {
"version": "file:packages/playwright-ct-core", "version": "file:packages/playwright-ct-core",
"requires": { "requires": {
"playwright": "1.40.0-next", "playwright": "1.40.1",
"playwright-core": "1.40.0-next", "playwright-core": "1.40.1",
"vite": "^4.4.10" "vite": "^4.4.10"
} }
}, },
"@playwright/experimental-ct-react": { "@playwright/experimental-ct-react": {
"version": "file:packages/playwright-ct-react", "version": "file:packages/playwright-ct-react",
"requires": { "requires": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@vitejs/plugin-react": "^4.0.0" "@vitejs/plugin-react": "^4.0.0"
}, },
"dependencies": { "dependencies": {
@ -8078,7 +8078,7 @@
"@playwright/experimental-ct-react17": { "@playwright/experimental-ct-react17": {
"version": "file:packages/playwright-ct-react17", "version": "file:packages/playwright-ct-react17",
"requires": { "requires": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@vitejs/plugin-react": "^4.0.0" "@vitejs/plugin-react": "^4.0.0"
}, },
"dependencies": { "dependencies": {
@ -8098,7 +8098,7 @@
"@playwright/experimental-ct-solid": { "@playwright/experimental-ct-solid": {
"version": "file:packages/playwright-ct-solid", "version": "file:packages/playwright-ct-solid",
"requires": { "requires": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"solid-js": "^1.7.0", "solid-js": "^1.7.0",
"vite-plugin-solid": "^2.7.0" "vite-plugin-solid": "^2.7.0"
} }
@ -8106,7 +8106,7 @@
"@playwright/experimental-ct-svelte": { "@playwright/experimental-ct-svelte": {
"version": "file:packages/playwright-ct-svelte", "version": "file:packages/playwright-ct-svelte",
"requires": { "requires": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@sveltejs/vite-plugin-svelte": "^2.1.1", "@sveltejs/vite-plugin-svelte": "^2.1.1",
"svelte": "^3.55.1" "svelte": "^3.55.1"
} }
@ -8114,7 +8114,7 @@
"@playwright/experimental-ct-vue": { "@playwright/experimental-ct-vue": {
"version": "file:packages/playwright-ct-vue", "version": "file:packages/playwright-ct-vue",
"requires": { "requires": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@vitejs/plugin-vue": "^4.2.1" "@vitejs/plugin-vue": "^4.2.1"
}, },
"dependencies": { "dependencies": {
@ -8148,7 +8148,7 @@
"@playwright/experimental-ct-vue2": { "@playwright/experimental-ct-vue2": {
"version": "file:packages/playwright-ct-vue2", "version": "file:packages/playwright-ct-vue2",
"requires": { "requires": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@vitejs/plugin-vue2": "^2.2.0", "@vitejs/plugin-vue2": "^2.2.0",
"vue": "^2.7.14" "vue": "^2.7.14"
} }
@ -8156,7 +8156,7 @@
"@playwright/test": { "@playwright/test": {
"version": "file:packages/playwright-test", "version": "file:packages/playwright-test",
"requires": { "requires": {
"playwright": "1.40.0-next" "playwright": "1.40.1"
} }
}, },
"@sindresorhus/is": { "@sindresorhus/is": {
@ -11000,13 +11000,13 @@
"version": "file:packages/playwright", "version": "file:packages/playwright",
"requires": { "requires": {
"fsevents": "2.3.2", "fsevents": "2.3.2",
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
} }
}, },
"playwright-chromium": { "playwright-chromium": {
"version": "file:packages/playwright-chromium", "version": "file:packages/playwright-chromium",
"requires": { "requires": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
} }
}, },
"playwright-core": { "playwright-core": {
@ -11015,13 +11015,13 @@
"playwright-firefox": { "playwright-firefox": {
"version": "file:packages/playwright-firefox", "version": "file:packages/playwright-firefox",
"requires": { "requires": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
} }
}, },
"playwright-webkit": { "playwright-webkit": {
"version": "file:packages/playwright-webkit", "version": "file:packages/playwright-webkit",
"requires": { "requires": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
} }
}, },
"postcss": { "postcss": {

View file

@ -1,7 +1,7 @@
{ {
"name": "playwright-internal", "name": "playwright-internal",
"private": true, "private": true,
"version": "1.40.0-next", "version": "1.40.1",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/browser-chromium", "name": "@playwright/browser-chromium",
"version": "1.40.0-next", "version": "1.40.1",
"description": "Playwright package that automatically installs Chromium", "description": "Playwright package that automatically installs Chromium",
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,6 +27,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/browser-firefox", "name": "@playwright/browser-firefox",
"version": "1.40.0-next", "version": "1.40.1",
"description": "Playwright package that automatically installs Firefox", "description": "Playwright package that automatically installs Firefox",
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,6 +27,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/browser-webkit", "name": "@playwright/browser-webkit",
"version": "1.40.0-next", "version": "1.40.1",
"description": "Playwright package that automatically installs WebKit", "description": "Playwright package that automatically installs WebKit",
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,6 +27,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-chromium", "name": "playwright-chromium",
"version": "1.40.0-next", "version": "1.40.1",
"description": "A high-level API to automate Chromium", "description": "A high-level API to automate Chromium",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,6 +30,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-core", "name": "playwright-core",
"version": "1.40.0-next", "version": "1.40.1",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -46,6 +46,7 @@ import type { Protocol } from './protocol';
import { VideoRecorder } from './videoRecorder'; import { VideoRecorder } from './videoRecorder';
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import { TargetClosedError } from '../errors'; import { TargetClosedError } from '../errors';
import { isSessionClosedError } from '../protocolError';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -132,7 +133,7 @@ export class CRPage implements PageDelegate {
return cb(frameSession); return cb(frameSession);
return cb(frameSession).catch(e => { return cb(frameSession).catch(e => {
// Broadcasting a message to the closed iframe should be a noop. // Broadcasting a message to the closed iframe should be a noop.
if (e.message && e.message.includes('Target closed')) if (isSessionClosedError(e))
return; return;
throw e; throw e;
}); });

View file

@ -44,9 +44,10 @@ x-pw-dialog {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: absolute; position: absolute;
width: 500px; width: 400px;
height: 200px; height: 150px;
z-index: 10; z-index: 10;
font-size: 13px;
} }
x-pw-dialog-body { x-pw-dialog-body {
@ -217,6 +218,15 @@ x-pw-tool-item.cancel > x-div {
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/></svg>"); mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/></svg>");
} }
x-pw-tool-item.succeeded > x-div {
/* codicon: pass */
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M6.27 10.87h.71l4.56-4.56-.71-.71-4.2 4.21-1.92-1.92L4 8.6l2.27 2.27z'/><path fill-rule='evenodd' clip-rule='evenodd' d='M8.6 1c1.6.1 3.1.9 4.2 2 1.3 1.4 2 3.1 2 5.1 0 1.6-.6 3.1-1.6 4.4-1 1.2-2.4 2.1-4 2.4-1.6.3-3.2.1-4.6-.7-1.4-.8-2.5-2-3.1-3.5C.9 9.2.8 7.5 1.3 6c.5-1.6 1.4-2.9 2.8-3.8C5.4 1.3 7 .9 8.6 1zm.5 12.9c1.3-.3 2.5-1 3.4-2.1.8-1.1 1.3-2.4 1.2-3.8 0-1.6-.6-3.2-1.7-4.3-1-1-2.2-1.6-3.6-1.7-1.3-.1-2.7.2-3.8 1-1.1.8-1.9 1.9-2.3 3.3-.4 1.3-.4 2.7.2 4 .6 1.3 1.5 2.3 2.7 3 1.2.7 2.6.9 3.9.6z'/></svg>") !important;
mask-image: url("data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg' fill='currentColor'><path d='M6.27 10.87h.71l4.56-4.56-.71-.71-4.2 4.21-1.92-1.92L4 8.6l2.27 2.27z'/><path fill-rule='evenodd' clip-rule='evenodd' d='M8.6 1c1.6.1 3.1.9 4.2 2 1.3 1.4 2 3.1 2 5.1 0 1.6-.6 3.1-1.6 4.4-1 1.2-2.4 2.1-4 2.4-1.6.3-3.2.1-4.6-.7-1.4-.8-2.5-2-3.1-3.5C.9 9.2.8 7.5 1.3 6c.5-1.6 1.4-2.9 2.8-3.8C5.4 1.3 7 .9 8.6 1zm.5 12.9c1.3-.3 2.5-1 3.4-2.1.8-1.1 1.3-2.4 1.2-3.8 0-1.6-.6-3.2-1.7-4.3-1-1-2.2-1.6-3.6-1.7-1.3-.1-2.7.2-3.8 1-1.1.8-1.9 1.9-2.3 3.3-.4 1.3-.4 2.7.2 4 .6 1.3 1.5 2.3 2.7 3 1.2.7 2.6.9 3.9.6z'/></svg>") !important;
background-color: #388a34 !important;
-webkit-mask-size: 18px !important;
mask-size: 18px !important;
}
x-pw-overlay { x-pw-overlay {
position: absolute; position: absolute;
top: 0; top: 0;
@ -238,13 +248,15 @@ x-pw-overlay x-pw-tool-item {
} }
textarea.text-editor { textarea.text-editor {
font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; font-family: system-ui,Ubuntu,Droid Sans,sans-serif;
flex: auto; flex: auto;
border: none; border: none;
margin: 6px; margin: 6px 10px;
color: #333; color: #333;
outline: 1px solid transparent !important; outline: 1px solid transparent!important;
resize: none; resize: none;
padding: 0;
font-size: 13px;
} }
textarea.text-editor.does-not-match { textarea.text-editor.does-not-match {

View file

@ -24,28 +24,8 @@ import { isInsideScope } from './domUtils';
import { elementText } from './selectorUtils'; import { elementText } from './selectorUtils';
import type { ElementText } from './selectorUtils'; import type { ElementText } from './selectorUtils';
import { asLocator } from '../../utils/isomorphic/locatorGenerators'; import { asLocator } from '../../utils/isomorphic/locatorGenerators';
import type { Language } from '../../utils/isomorphic/locatorGenerators';
import { locatorOrSelectorAsSelector } from '@isomorphic/locatorParser';
import { parseSelector } from '@isomorphic/selectorParser';
import { normalizeWhiteSpace } from '@isomorphic/stringUtils'; import { normalizeWhiteSpace } from '@isomorphic/stringUtils';
// @ts-ignore @no-check-deps
import CodeMirrorImpl from 'codemirror-shadow-1';
import type CodeMirrorType from 'codemirror';
// @no-check-deps
import codemirrorCSS from 'codemirror-shadow-1/lib/codemirror.css?inline';
// @no-check-deps
import 'codemirror-shadow-1/mode/css/css';
// @no-check-deps
import 'codemirror-shadow-1/mode/htmlmixed/htmlmixed';
// @no-check-deps
import 'codemirror-shadow-1/mode/javascript/javascript';
// @no-check-deps
import 'codemirror-shadow-1/mode/python/python';
// @no-check-deps
import 'codemirror-shadow-1/mode/clike/clike';
const CodeMirror = CodeMirrorImpl as typeof CodeMirrorType;
interface RecorderDelegate { interface RecorderDelegate {
performAction?(action: actions.Action): Promise<void>; performAction?(action: actions.Action): Promise<void>;
recordAction?(action: actions.Action): Promise<void>; recordAction?(action: actions.Action): Promise<void>;
@ -68,6 +48,7 @@ interface RecorderTool {
onMouseDown?(event: MouseEvent): void; onMouseDown?(event: MouseEvent): void;
onMouseUp?(event: MouseEvent): void; onMouseUp?(event: MouseEvent): void;
onMouseMove?(event: MouseEvent): void; onMouseMove?(event: MouseEvent): void;
onMouseEnter?(event: MouseEvent): void;
onMouseLeave?(event: MouseEvent): void; onMouseLeave?(event: MouseEvent): void;
onFocus?(event: Event): void; onFocus?(event: Event): void;
onScroll?(event: Event): void; onScroll?(event: Event): void;
@ -109,6 +90,7 @@ class InspectTool implements RecorderTool {
signals: [], signals: [],
}); });
this._recorder.delegate.setMode?.('recording'); this._recorder.delegate.setMode?.('recording');
this._recorder.overlay?.flashToolSucceeded('assertingVisibility');
} }
} else { } else {
this._recorder.delegate.setSelector?.(this._hoveredModel ? this._hoveredModel.selector : ''); this._recorder.delegate.setSelector?.(this._hoveredModel ? this._hoveredModel.selector : '');
@ -146,6 +128,10 @@ class InspectTool implements RecorderTool {
this._recorder.updateHighlight(model, true, { color: this._assertVisibility ? '#8acae480' : undefined }); this._recorder.updateHighlight(model, true, { color: this._assertVisibility ? '#8acae480' : undefined });
} }
onMouseEnter(event: MouseEvent) {
consumeEvent(event);
}
onMouseLeave(event: MouseEvent) { onMouseLeave(event: MouseEvent) {
consumeEvent(event); consumeEvent(event);
const window = this._recorder.injectedScript.window; const window = this._recorder.injectedScript.window;
@ -518,14 +504,23 @@ class TextAssertionTool implements RecorderTool {
} }
onClick(event: MouseEvent) { onClick(event: MouseEvent) {
if (!this._dialogElement)
this._showDialog();
consumeEvent(event); consumeEvent(event);
if (this._kind === 'value') {
const action = this._generateAction();
if (action) {
this._recorder.delegate.recordAction?.(action);
this._recorder.delegate.setMode?.('recording');
this._recorder.overlay?.flashToolSucceeded('assertingValue');
}
} else {
if (!this._dialogElement)
this._showDialog();
}
} }
onMouseDown(event: MouseEvent) { onMouseDown(event: MouseEvent) {
const target = this._recorder.deepEventTarget(event); const target = this._recorder.deepEventTarget(event);
if (target.nodeName === 'SELECT') if (this._elementHasValue(target))
event.preventDefault(); event.preventDefault();
} }
@ -618,7 +613,7 @@ class TextAssertionTool implements RecorderTool {
if (!this._hoverHighlight?.elements[0]) if (!this._hoverHighlight?.elements[0])
return; return;
this._action = this._generateAction(); this._action = this._generateAction();
if (!this._action) if (!this._action || this._action.name !== 'assertText')
return; return;
this._dialogElement = this._recorder.document.createElement('x-pw-dialog'); this._dialogElement = this._recorder.document.createElement('x-pw-dialog');
@ -636,122 +631,41 @@ class TextAssertionTool implements RecorderTool {
this._recorder.document.addEventListener('keydown', this._keyboardListener, true); this._recorder.document.addEventListener('keydown', this._keyboardListener, true);
const toolbarElement = this._recorder.document.createElement('x-pw-tools-list'); const toolbarElement = this._recorder.document.createElement('x-pw-tools-list');
toolbarElement.appendChild(this._createLabel(this._action)); const labelElement = this._recorder.document.createElement('label');
labelElement.textContent = 'Assert that element contains text';
toolbarElement.appendChild(labelElement);
toolbarElement.appendChild(this._recorder.document.createElement('x-spacer')); toolbarElement.appendChild(this._recorder.document.createElement('x-spacer'));
toolbarElement.appendChild(this._acceptButton); toolbarElement.appendChild(this._acceptButton);
toolbarElement.appendChild(this._cancelButton); toolbarElement.appendChild(this._cancelButton);
this._dialogElement.appendChild(toolbarElement); this._dialogElement.appendChild(toolbarElement);
const bodyElement = this._recorder.document.createElement('x-pw-dialog-body'); const bodyElement = this._recorder.document.createElement('x-pw-dialog-body');
const cmStyle = this._recorder.document.createElement('style');
const cmElement = this._recorder.document.createElement('x-locator-editor');
cmStyle.textContent = codemirrorCSS;
bodyElement.appendChild(cmStyle);
bodyElement.appendChild(cmElement);
const cm = CodeMirror(cmElement, {
value: asLocator(this._recorder.state.language, this._action.selector),
mode: cmModeForLanguage(this._recorder.state.language),
readOnly: false,
lineNumbers: false,
lineWrapping: true,
});
cm.on('keydown', (_, event) => {
if (event.key === 'Tab')
(event as any).codemirrorIgnore = true;
});
cm.on('change', () => {
if (this._action) {
const selector = locatorOrSelectorAsSelector(this._recorder.state.language, cm.getValue(), this._recorder.state.testIdAttributeName);
let elements: Element[] = [];
try {
elements = this._recorder.injectedScript.querySelectorAll(parseSelector(selector), this._recorder.document);
} catch {
}
cmElement.classList.toggle('does-not-match', !elements.length);
this._hoverHighlight = elements.length ? {
selector,
elements,
} : null;
this._action.selector = selector;
this._recorder.updateHighlight(this._hoverHighlight, true);
}
});
let elementToFocus: HTMLElement | null = null;
const action = this._action; const action = this._action;
if (action.name === 'assertText') { const textElement = this._recorder.document.createElement('textarea');
const textElement = this._recorder.document.createElement('textarea'); textElement.setAttribute('spellcheck', 'false');
textElement.setAttribute('spellcheck', 'false'); textElement.value = this._renderValue(this._action);
textElement.value = this._renderValue(this._action); textElement.classList.add('text-editor');
textElement.classList.add('text-editor');
const updateAndValidate = () => { const updateAndValidate = () => {
const newValue = normalizeWhiteSpace(textElement.value); const newValue = normalizeWhiteSpace(textElement.value);
const target = this._hoverHighlight?.elements[0]; const target = this._hoverHighlight?.elements[0];
if (!target) if (!target)
return; return;
action.text = newValue; action.text = newValue;
const targetText = normalizeWhiteSpace(elementText(this._textCache, target).full); const targetText = normalizeWhiteSpace(elementText(this._textCache, target).full);
const matches = action.substring ? newValue && targetText.includes(newValue) : targetText === newValue; const matches = newValue && targetText.includes(newValue);
textElement.classList.toggle('does-not-match', !matches); textElement.classList.toggle('does-not-match', !matches);
}; };
textElement.addEventListener('input', updateAndValidate); textElement.addEventListener('input', updateAndValidate);
bodyElement.appendChild(textElement); bodyElement.appendChild(textElement);
// Add a toolbar substring checkbox.
const substringElement = this._recorder.document.createElement('label');
substringElement.style.cursor = 'pointer';
const checkboxElement = this._recorder.document.createElement('input');
substringElement.appendChild(checkboxElement);
substringElement.appendChild(this._recorder.document.createTextNode('Substring'));
checkboxElement.type = 'checkbox';
checkboxElement.style.cursor = 'pointer';
checkboxElement.checked = action.substring;
checkboxElement.addEventListener('change', () => {
action.substring = checkboxElement.checked;
updateAndValidate();
});
toolbarElement.insertBefore(substringElement, this._acceptButton);
elementToFocus = textElement;
} else if (action.name === 'assertValue') {
const textElement = this._recorder.document.createElement('textarea');
textElement.setAttribute('spellcheck', 'false');
textElement.value = this._renderValue(this._action);
textElement.classList.add('text-editor');
textElement.addEventListener('input', () => {
action.value = textElement.value;
});
bodyElement.appendChild(textElement);
elementToFocus = textElement;
} else if (action.name === 'assertChecked') {
const labelElement = this._recorder.document.createElement('label');
labelElement.textContent = 'Value:';
const checkboxElement = this._recorder.document.createElement('input');
labelElement.appendChild(checkboxElement);
checkboxElement.type = 'checkbox';
checkboxElement.checked = action.checked;
checkboxElement.addEventListener('change', () => {
action.checked = checkboxElement.checked;
});
bodyElement.appendChild(labelElement);
elementToFocus = labelElement;
}
this._dialogElement.appendChild(bodyElement); this._dialogElement.appendChild(bodyElement);
this._recorder.highlight.appendChild(this._dialogElement); this._recorder.highlight.appendChild(this._dialogElement);
const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, this._dialogElement); const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, this._dialogElement);
this._dialogElement.style.top = position.anchorTop + 'px'; this._dialogElement.style.top = position.anchorTop + 'px';
this._dialogElement.style.left = position.anchorLeft + 'px'; this._dialogElement.style.left = position.anchorLeft + 'px';
elementToFocus?.focus(); textElement.focus();
cm.refresh();
}
private _createLabel(action: actions.AssertAction) {
const labelElement = this._recorder.document.createElement('label');
labelElement.textContent = action.name === 'assertText' ? 'Assert text' : action.name === 'assertValue' ? 'Assert value' : 'Assert checked';
return labelElement;
} }
private _closeDialog() { private _closeDialog() {
@ -829,7 +743,7 @@ class Overlay {
toolsListElement.appendChild(this._assertVisibilityToggle); toolsListElement.appendChild(this._assertVisibilityToggle);
this._assertTextToggle = this._recorder.injectedScript.document.createElement('x-pw-tool-item'); this._assertTextToggle = this._recorder.injectedScript.document.createElement('x-pw-tool-item');
this._assertTextToggle.title = 'Assert text and values'; this._assertTextToggle.title = 'Assert text';
this._assertTextToggle.classList.add('text'); this._assertTextToggle.classList.add('text');
this._assertTextToggle.appendChild(this._recorder.injectedScript.document.createElement('x-div')); this._assertTextToggle.appendChild(this._recorder.injectedScript.document.createElement('x-div'));
this._assertTextToggle.addEventListener('click', () => { this._assertTextToggle.addEventListener('click', () => {
@ -853,7 +767,7 @@ class Overlay {
install() { install() {
this._recorder.highlight.appendChild(this._overlayElement); this._recorder.highlight.appendChild(this._overlayElement);
this._measure = this._overlayElement.getBoundingClientRect(); this._updateVisualPosition();
} }
contains(element: Element) { contains(element: Element) {
@ -874,13 +788,31 @@ class Overlay {
this._updateVisualPosition(); this._updateVisualPosition();
} }
if (state.mode === 'none') if (state.mode === 'none')
this._overlayElement.setAttribute('hidden', 'true'); this._hideOverlay();
else else
this._overlayElement.removeAttribute('hidden'); this._showOverlay();
}
flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue') {
const element = tool === 'assertingVisibility' ? this._assertVisibilityToggle : this._assertValuesToggle;
element.classList.add('succeeded');
setTimeout(() => element.classList.remove('succeeded'), 2000);
}
private _hideOverlay() {
this._overlayElement.setAttribute('hidden', 'true');
}
private _showOverlay() {
if (!this._overlayElement.hasAttribute('hidden'))
return;
this._overlayElement.removeAttribute('hidden');
this._updateVisualPosition();
} }
private _updateVisualPosition() { private _updateVisualPosition() {
this._overlayElement.style.left = (this._recorder.injectedScript.window.innerWidth / 2 + this._offsetX) + 'px'; this._measure = this._overlayElement.getBoundingClientRect();
this._overlayElement.style.left = ((this._recorder.injectedScript.window.innerWidth - this._measure.width) / 2 + this._offsetX) + 'px';
} }
onMouseMove(event: MouseEvent) { onMouseMove(event: MouseEvent) {
@ -890,8 +822,8 @@ class Overlay {
} }
if (this._dragState) { if (this._dragState) {
this._offsetX = this._dragState.offsetX + event.clientX - this._dragState.dragStart.x; this._offsetX = this._dragState.offsetX + event.clientX - this._dragState.dragStart.x;
this._offsetX = Math.min(this._recorder.injectedScript.window.innerWidth / 2 - 10 - this._measure.width, this._offsetX); const halfGapSize = (this._recorder.injectedScript.window.innerWidth - this._measure.width) / 2 - 10;
this._offsetX = Math.max(10 - this._recorder.injectedScript.window.innerWidth / 2, this._offsetX); this._offsetX = Math.max(-halfGapSize, Math.min(halfGapSize, this._offsetX));
this._updateVisualPosition(); this._updateVisualPosition();
this._recorder.delegate.setOverlayState?.({ offsetX: this._offsetX }); this._recorder.delegate.setOverlayState?.({ offsetX: this._offsetX });
consumeEvent(event); consumeEvent(event);
@ -925,7 +857,7 @@ export class Recorder {
private _tools: Record<Mode, RecorderTool>; private _tools: Record<Mode, RecorderTool>;
private _actionSelectorModel: HighlightModel | null = null; private _actionSelectorModel: HighlightModel | null = null;
readonly highlight: Highlight; readonly highlight: Highlight;
private _overlay: Overlay | undefined; readonly overlay: Overlay | undefined;
private _styleElement: HTMLStyleElement; private _styleElement: HTMLStyleElement;
state: UIState = { mode: 'none', testIdAttributeName: 'data-testid', language: 'javascript', overlay: { offsetX: 0 } }; state: UIState = { mode: 'none', testIdAttributeName: 'data-testid', language: 'javascript', overlay: { offsetX: 0 } };
readonly document: Document; readonly document: Document;
@ -947,8 +879,8 @@ export class Recorder {
}; };
this._currentTool = this._tools.none; this._currentTool = this._tools.none;
if (injectedScript.window.top === injectedScript.window) { if (injectedScript.window.top === injectedScript.window) {
this._overlay = new Overlay(this); this.overlay = new Overlay(this);
this._overlay.setUIState(this.state); this.overlay.setUIState(this.state);
} }
this._styleElement = this.document.createElement('style'); this._styleElement = this.document.createElement('style');
this._styleElement.textContent = ` this._styleElement.textContent = `
@ -976,11 +908,12 @@ export class Recorder {
addEventListener(this.document, 'mouseup', event => this._onMouseUp(event as MouseEvent), true), addEventListener(this.document, 'mouseup', event => this._onMouseUp(event as MouseEvent), true),
addEventListener(this.document, 'mousemove', event => this._onMouseMove(event as MouseEvent), true), addEventListener(this.document, 'mousemove', event => this._onMouseMove(event as MouseEvent), true),
addEventListener(this.document, 'mouseleave', event => this._onMouseLeave(event as MouseEvent), true), addEventListener(this.document, 'mouseleave', event => this._onMouseLeave(event as MouseEvent), true),
addEventListener(this.document, 'mouseenter', event => this._onMouseEnter(event as MouseEvent), true),
addEventListener(this.document, 'focus', event => this._onFocus(event), true), addEventListener(this.document, 'focus', event => this._onFocus(event), true),
addEventListener(this.document, 'scroll', event => this._onScroll(event), true), addEventListener(this.document, 'scroll', event => this._onScroll(event), true),
]; ];
this.highlight.install(); this.highlight.install();
this._overlay?.install(); this.overlay?.install();
this.injectedScript.document.head.appendChild(this._styleElement); this.injectedScript.document.head.appendChild(this._styleElement);
} }
@ -1011,7 +944,7 @@ export class Recorder {
this.state = state; this.state = state;
this.highlight.setLanguage(state.language); this.highlight.setLanguage(state.language);
this._switchCurrentTool(); this._switchCurrentTool();
this._overlay?.setUIState(state); this.overlay?.setUIState(state);
// Race or scroll. // Race or scroll.
if (this._actionSelectorModel?.selector && !this._actionSelectorModel?.elements.length) if (this._actionSelectorModel?.selector && !this._actionSelectorModel?.elements.length)
@ -1030,7 +963,7 @@ export class Recorder {
private _onClick(event: MouseEvent) { private _onClick(event: MouseEvent) {
if (!event.isTrusted) if (!event.isTrusted)
return; return;
if (this._overlay?.onClick(event)) if (this.overlay?.onClick(event))
return; return;
if (this._ignoreOverlayEvent(event)) if (this._ignoreOverlayEvent(event))
return; return;
@ -1072,7 +1005,7 @@ export class Recorder {
private _onMouseUp(event: MouseEvent) { private _onMouseUp(event: MouseEvent) {
if (!event.isTrusted) if (!event.isTrusted)
return; return;
if (this._overlay?.onMouseUp(event)) if (this.overlay?.onMouseUp(event))
return; return;
if (this._ignoreOverlayEvent(event)) if (this._ignoreOverlayEvent(event))
return; return;
@ -1082,13 +1015,21 @@ export class Recorder {
private _onMouseMove(event: MouseEvent) { private _onMouseMove(event: MouseEvent) {
if (!event.isTrusted) if (!event.isTrusted)
return; return;
if (this._overlay?.onMouseMove(event)) if (this.overlay?.onMouseMove(event))
return; return;
if (this._ignoreOverlayEvent(event)) if (this._ignoreOverlayEvent(event))
return; return;
this._currentTool.onMouseMove?.(event); this._currentTool.onMouseMove?.(event);
} }
private _onMouseEnter(event: MouseEvent) {
if (!event.isTrusted)
return;
if (this._ignoreOverlayEvent(event))
return;
this._currentTool.onMouseEnter?.(event);
}
private _onMouseLeave(event: MouseEvent) { private _onMouseLeave(event: MouseEvent) {
if (!event.isTrusted) if (!event.isTrusted)
return; return;
@ -1149,7 +1090,7 @@ export class Recorder {
deepEventTarget(event: Event): HTMLElement { deepEventTarget(event: Event): HTMLElement {
for (const element of event.composedPath()) { for (const element of event.composedPath()) {
if (!this._overlay?.contains(element as Element)) if (!this.overlay?.contains(element as Element))
return element as HTMLElement; return element as HTMLElement;
} }
return event.composedPath()[0] as HTMLElement; return event.composedPath()[0] as HTMLElement;
@ -1301,14 +1242,4 @@ export class PollingRecorder implements RecorderDelegate {
} }
} }
function cmModeForLanguage(language: Language): string {
if (language === 'python')
return 'python';
if (language === 'java')
return 'text/x-java';
if (language === 'csharp')
return 'text/x-csharp';
return 'javascript';
}
export default PollingRecorder; export default PollingRecorder;

View file

@ -373,6 +373,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
} }
onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string) { onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string) {
if (metadata.isServerSide || metadata.internal)
return;
if (logName !== 'api') if (logName !== 'api')
return; return;
const event = createActionLogTraceEvent(metadata, message); const event = createActionLogTraceEvent(metadata, message);

View file

@ -28,8 +28,8 @@ export async function mkdirIfNeeded(filePath: string) {
export async function removeFolders(dirs: string[]): Promise<Error[]> { export async function removeFolders(dirs: string[]): Promise<Error[]> {
return await Promise.all(dirs.map((dir: string) => return await Promise.all(dirs.map((dir: string) =>
fs.promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }) fs.promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch(e => e)
)).catch(e => e); ));
} }
export function canAccessFile(file: string) { export function canAccessFile(file: string) {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-core", "name": "@playwright/experimental-ct-core",
"version": "1.40.0-next", "version": "1.40.1",
"description": "Playwright Component Testing Helpers", "description": "Playwright Component Testing Helpers",
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,9 +27,9 @@
} }
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next", "playwright-core": "1.40.1",
"vite": "^4.4.10", "vite": "^4.4.10",
"playwright": "1.40.0-next" "playwright": "1.40.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-react", "name": "@playwright/experimental-ct-react",
"version": "1.40.0-next", "version": "1.40.1",
"description": "Playwright Component Testing for React", "description": "Playwright Component Testing for React",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@vitejs/plugin-react": "^4.0.0" "@vitejs/plugin-react": "^4.0.0"
}, },
"bin": { "bin": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-react17", "name": "@playwright/experimental-ct-react17",
"version": "1.40.0-next", "version": "1.40.1",
"description": "Playwright Component Testing for React", "description": "Playwright Component Testing for React",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@vitejs/plugin-react": "^4.0.0" "@vitejs/plugin-react": "^4.0.0"
}, },
"bin": { "bin": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-solid", "name": "@playwright/experimental-ct-solid",
"version": "1.40.0-next", "version": "1.40.1",
"description": "Playwright Component Testing for Solid", "description": "Playwright Component Testing for Solid",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"vite-plugin-solid": "^2.7.0" "vite-plugin-solid": "^2.7.0"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-svelte", "name": "@playwright/experimental-ct-svelte",
"version": "1.40.0-next", "version": "1.40.1",
"description": "Playwright Component Testing for Svelte", "description": "Playwright Component Testing for Svelte",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@sveltejs/vite-plugin-svelte": "^2.1.1" "@sveltejs/vite-plugin-svelte": "^2.1.1"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-vue", "name": "@playwright/experimental-ct-vue",
"version": "1.40.0-next", "version": "1.40.1",
"description": "Playwright Component Testing for Vue", "description": "Playwright Component Testing for Vue",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@vitejs/plugin-vue": "^4.2.1" "@vitejs/plugin-vue": "^4.2.1"
}, },
"bin": { "bin": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-vue2", "name": "@playwright/experimental-ct-vue2",
"version": "1.40.0-next", "version": "1.40.1",
"description": "Playwright Component Testing for Vue2", "description": "Playwright Component Testing for Vue2",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.40.0-next", "@playwright/experimental-ct-core": "1.40.1",
"@vitejs/plugin-vue2": "^2.2.0" "@vitejs/plugin-vue2": "^2.2.0"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-firefox", "name": "playwright-firefox",
"version": "1.40.0-next", "version": "1.40.1",
"description": "A high-level API to automate Firefox", "description": "A high-level API to automate Firefox",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,6 +30,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/test", "name": "@playwright/test",
"version": "1.40.0-next", "version": "1.40.1",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,6 +30,6 @@
}, },
"scripts": {}, "scripts": {},
"dependencies": { "dependencies": {
"playwright": "1.40.0-next" "playwright": "1.40.1"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-webkit", "name": "playwright-webkit",
"version": "1.40.0-next", "version": "1.40.1",
"description": "A high-level API to automate WebKit", "description": "A high-level API to automate WebKit",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,6 +30,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright", "name": "playwright",
"version": "1.40.0-next", "version": "1.40.1",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",
@ -54,7 +54,7 @@
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.40.0-next" "playwright-core": "1.40.1"
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "2.3.2" "fsevents": "2.3.2"

View file

@ -239,9 +239,6 @@ export class TestCase extends Base implements reporterTypes.TestCase {
_poolDigest = ''; _poolDigest = '';
_workerHash = ''; _workerHash = '';
_projectId = ''; _projectId = '';
// This is different from |results.length| because sometimes we do not run the test, but consume
// an attempt, for example when skipping tests in a serial suite after a failure.
_runAttempts = 0;
// Annotations known statically before running the test, e.g. `test.skip()` or `test.describe.skip()`. // Annotations known statically before running the test, e.g. `test.skip()` or `test.describe.skip()`.
_staticAnnotations: Annotation[] = []; _staticAnnotations: Annotation[] = [];
@ -259,10 +256,16 @@ export class TestCase extends Base implements reporterTypes.TestCase {
} }
outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky' { outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky' {
const results = this.results.filter(result => result.status !== 'interrupted'); // Ignore initial skips that may be a result of "skipped because previous test in serial mode failed".
if (results.every(result => result.status === 'skipped')) const results = [...this.results];
while (results[0]?.status === 'skipped' || results[0]?.status === 'interrupted')
results.shift();
// All runs were skipped.
if (!results.length)
return 'skipped'; return 'skipped';
const failures = results.filter(result => result.status !== this.expectedStatus);
const failures = results.filter(result => result.status !== 'skipped' && result.status !== 'interrupted' && result.status !== this.expectedStatus);
if (!failures.length) // all passed if (!failures.length) // all passed
return 'expected'; return 'expected';
if (failures.length === results.length) // all failed if (failures.length === results.length) // all failed

View file

@ -492,10 +492,16 @@ export class TeleTestCase implements reporterTypes.TestCase {
} }
outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky' { outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky' {
const results = this.results.filter(result => result.status !== 'interrupted'); // Ignore initial skips that may be a result of "skipped because previous test in serial mode failed".
if (results.every(result => result.status === 'skipped')) const results = [...this.results];
while (results[0]?.status === 'skipped' || results[0]?.status === 'interrupted')
results.shift();
// All runs were skipped.
if (!results.length)
return 'skipped'; return 'skipped';
const failures = results.filter(result => result.status !== this.expectedStatus);
const failures = results.filter(result => result.status !== 'skipped' && result.status !== 'interrupted' && result.status !== this.expectedStatus);
if (!failures.length) // all passed if (!failures.length) // all passed
return 'expected'; return 'expected';
if (failures.length === results.length) // all failed if (failures.length === results.length) // all failed

View file

@ -92,19 +92,37 @@ const commonEvents = new Set(commonEventNames);
const commonEventRegex = new RegExp(`${commonEventNames.join('|')}`); const commonEventRegex = new RegExp(`${commonEventNames.join('|')}`);
function parseCommonEvents(reportJsonl: Buffer): JsonEvent[] { function parseCommonEvents(reportJsonl: Buffer): JsonEvent[] {
return reportJsonl.toString().split('\n') return splitBufferLines(reportJsonl)
.map(line => line.toString('utf8'))
.filter(line => commonEventRegex.test(line)) // quick filter .filter(line => commonEventRegex.test(line)) // quick filter
.map(line => JSON.parse(line) as JsonEvent) .map(line => JSON.parse(line) as JsonEvent)
.filter(event => commonEvents.has(event.method)); .filter(event => commonEvents.has(event.method));
} }
function parseTestEvents(reportJsonl: Buffer): JsonEvent[] { function parseTestEvents(reportJsonl: Buffer): JsonEvent[] {
return reportJsonl.toString().split('\n') return splitBufferLines(reportJsonl)
.map(line => line.toString('utf8'))
.filter(line => line.length) .filter(line => line.length)
.map(line => JSON.parse(line) as JsonEvent) .map(line => JSON.parse(line) as JsonEvent)
.filter(event => !commonEvents.has(event.method)); .filter(event => !commonEvents.has(event.method));
} }
function splitBufferLines(buffer: Buffer) {
const lines = [];
let start = 0;
while (start < buffer.length) {
// 0x0A is the byte for '\n'
const end = buffer.indexOf(0x0A, start);
if (end === -1) {
lines.push(buffer.slice(start));
break;
}
lines.push(buffer.slice(start, end));
start = end + 1;
}
return lines;
}
async function extractAndParseReports(dir: string, shardFiles: string[], internalizer: JsonStringInternalizer, printStatus: StatusCallback) { async function extractAndParseReports(dir: string, shardFiles: string[], internalizer: JsonStringInternalizer, printStatus: StatusCallback) {
const shardEvents: { file: string, localPath: string, metadata: BlobReportMetadata, parsedEvents: JsonEvent[] }[] = []; const shardEvents: { file: string, localPath: string, metadata: BlobReportMetadata, parsedEvents: JsonEvent[] }[] = [];
await fs.promises.mkdir(path.join(dir, 'resources'), { recursive: true }); await fs.promises.mkdir(path.join(dir, 'resources'), { recursive: true });

View file

@ -290,7 +290,7 @@ class JobDispatcher {
test.expectedStatus = params.expectedStatus; test.expectedStatus = params.expectedStatus;
test.annotations = params.annotations; test.annotations = params.annotations;
test.timeout = params.timeout; test.timeout = params.timeout;
const isFailure = result.status !== test.expectedStatus; const isFailure = result.status !== 'skipped' && result.status !== test.expectedStatus;
if (isFailure) if (isFailure)
this._failedTests.add(test); this._failedTests.add(test);
this._reportTestEnd(test, result); this._reportTestEnd(test, result);
@ -369,17 +369,22 @@ class JobDispatcher {
} }
result.errors = [...errors]; result.errors = [...errors];
result.error = result.errors[0]; result.error = result.errors[0];
result.status = 'failed'; result.status = errors.length ? 'failed' : 'skipped';
this._reportTestEnd(test, result); this._reportTestEnd(test, result);
this._failedTests.add(test); this._failedTests.add(test);
} }
private _handleFatalErrors(errors: TestError[]) { private _massSkipTestsFromRemaining(testIds: Set<string>, errors: TestError[]) {
const test = this._remainingByTestId.values().next().value as TestCase | undefined; for (const test of this._remainingByTestId.values()) {
if (test) { if (!testIds.has(test.id))
this._failTestWithErrors(test, errors); continue;
if (!this._failureTracker.hasReachedMaxFailures()) {
this._failTestWithErrors(test, errors);
errors = []; // Only report errors for the first test.
}
this._remainingByTestId.delete(test.id); this._remainingByTestId.delete(test.id);
} else if (errors.length) { }
if (errors.length) {
// We had fatal errors after all tests have passed - most likely in some teardown. // We had fatal errors after all tests have passed - most likely in some teardown.
// Let's just fail the test run. // Let's just fail the test run.
this._failureTracker.onWorkerError(); this._failureTracker.onWorkerError();
@ -407,28 +412,23 @@ class JobDispatcher {
} }
if (params.fatalErrors.length) { if (params.fatalErrors.length) {
// In case of fatal errors, report the first remaining test as failing with these errors, // In case of fatal errors, report first remaining test as failing with these errors,
// and "skip" all other tests to avoid running into the same issue over and over. // and all others as skipped.
this._handleFatalErrors(params.fatalErrors); this._massSkipTestsFromRemaining(new Set(this._remainingByTestId.keys()), params.fatalErrors);
this._remainingByTestId.clear();
} }
// Handle tests that should be skipped because of the setup failure. // Handle tests that should be skipped because of the setup failure.
for (const testId of params.skipTestsDueToSetupFailure) this._massSkipTestsFromRemaining(new Set(params.skipTestsDueToSetupFailure), []);
this._remainingByTestId.delete(testId);
if (params.unexpectedExitError) { if (params.unexpectedExitError) {
if (this._currentlyRunning) { // When worker exits during a test, we blame the test itself.
// When worker exits during a test, we blame the test itself. //
this._failTestWithErrors(this._currentlyRunning.test, [params.unexpectedExitError]); // The most common situation when worker exits while not running a test is:
this._remainingByTestId.delete(this._currentlyRunning.test.id); // worker failed to require the test file (at the start) because of an exception in one of imports.
} else { // In this case, "skip" all remaining tests, to avoid running into the same exception over and over.
// The most common situation when worker exits while not running a test is: if (this._currentlyRunning)
// worker failed to require the test file (at the start) because of an exception in one of imports. this._massSkipTestsFromRemaining(new Set([this._currentlyRunning.test.id]), [params.unexpectedExitError]);
// In this case, "skip" all remaining tests, to avoid running into the same exception over and over. else
this._handleFatalErrors([params.unexpectedExitError]); this._massSkipTestsFromRemaining(new Set(this._remainingByTestId.keys()), [params.unexpectedExitError]);
this._remainingByTestId.clear();
}
} }
const retryCandidates = new Set<TestCase>(); const retryCandidates = new Set<TestCase>();
@ -446,22 +446,26 @@ class JobDispatcher {
serialSuitesWithFailures.add(outermostSerialSuite); serialSuitesWithFailures.add(outermostSerialSuite);
} }
// If we have failed tests that belong to a serial suite,
// we should skip all future tests from the same serial suite.
const testsBelongingToSomeSerialSuiteWithFailures = [...this._remainingByTestId.values()].filter(test => {
let parent: Suite | undefined = test.parent;
while (parent && !serialSuitesWithFailures.has(parent))
parent = parent.parent;
return !!parent;
});
this._massSkipTestsFromRemaining(new Set(testsBelongingToSomeSerialSuiteWithFailures.map(test => test.id)), []);
for (const serialSuite of serialSuitesWithFailures) { for (const serialSuite of serialSuitesWithFailures) {
serialSuite.allTests().forEach(test => { // Add all tests from failed serial suites for possible retry.
// Skip remaining tests from serial suites with failures. // These will only be retried together, because they have the same
this._remainingByTestId.delete(test.id); // "retries" setting and the same number of previous runs.
// Schedule them for the retry all together. serialSuite.allTests().forEach(test => retryCandidates.add(test));
retryCandidates.add(test);
});
} }
for (const test of this._job.tests) {
if (!this._remainingByTestId.has(test.id))
++test._runAttempts;
}
const remaining = [...this._remainingByTestId.values()]; const remaining = [...this._remainingByTestId.values()];
for (const test of retryCandidates) { for (const test of retryCandidates) {
if (test._runAttempts < test.retries + 1) if (test.results.length < test.retries + 1)
remaining.push(test); remaining.push(test);
} }

View file

@ -31,7 +31,7 @@ export class FailureTracker {
} }
onTestEnd(test: TestCase, result: TestResult) { onTestEnd(test: TestCase, result: TestResult) {
if (result.status !== test.expectedStatus) if (result.status !== 'skipped' && result.status !== test.expectedStatus)
++this._failureCount; ++this._failureCount;
} }

View file

@ -190,7 +190,10 @@ export class TraceModel {
} }
case 'log': { case 'log': {
const existing = actionMap.get(event.callId); const existing = actionMap.get(event.callId);
existing!.log.push({ // We have some corrupted traces out there, tolerate them.
if (!existing)
return;
existing.log.push({
time: event.time, time: event.time,
message: event.message, message: event.message,
}); });

View file

@ -399,7 +399,7 @@ test('beforeAll failure should prevent the test, but not afterAll', async ({ run
}); });
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
expect(result.didNotRun).toBe(1); expect(result.skipped).toBe(1);
expect(result.outputLines).toEqual([ expect(result.outputLines).toEqual([
'beforeAll', 'beforeAll',
'afterAll', 'afterAll',
@ -499,7 +499,7 @@ test('beforeAll timeout should be reported and prevent more tests', async ({ run
}, { timeout: 1000 }); }, { timeout: 1000 });
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
expect(result.didNotRun).toBe(1); expect(result.skipped).toBe(1);
expect(result.outputLines).toEqual([ expect(result.outputLines).toEqual([
'beforeAll', 'beforeAll',
'afterAll', 'afterAll',
@ -688,7 +688,7 @@ test('unhandled rejection during beforeAll should be reported and prevent more t
}); });
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
expect(result.didNotRun).toBe(1); expect(result.skipped).toBe(1);
expect(result.outputLines).toEqual([ expect(result.outputLines).toEqual([
'beforeAll', 'beforeAll',
'afterAll', 'afterAll',
@ -801,7 +801,7 @@ test('beforeAll failure should only prevent tests that are affected', async ({ r
}); });
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
expect(result.didNotRun).toBe(1); expect(result.skipped).toBe(1);
expect(result.passed).toBe(1); expect(result.passed).toBe(1);
expect(result.outputLines).toEqual([ expect(result.outputLines).toEqual([
'beforeAll', 'beforeAll',

View file

@ -216,8 +216,8 @@ test('should retry beforeAll failure', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.passed).toBe(0); expect(result.passed).toBe(0);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
expect(result.didNotRun).toBe(1); expect(result.skipped).toBe(1);
expect(result.output.split('\n')[2]).toBe('××F'); expect(result.output.split('\n')[2]).toBe('×°×°F°');
expect(result.output).toContain('BeforeAll is bugged!'); expect(result.output).toContain('BeforeAll is bugged!');
}); });

View file

@ -111,7 +111,7 @@ test('should report subprocess creation error', async ({ runInlineTest }, testIn
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.passed).toBe(0); expect(result.passed).toBe(0);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
expect(result.didNotRun).toBe(1); expect(result.skipped).toBe(1);
expect(result.output).toContain('Error: worker process exited unexpectedly (code=42, signal=null)'); expect(result.output).toContain('Error: worker process exited unexpectedly (code=42, signal=null)');
}); });
@ -634,9 +634,9 @@ test('should not hang on worker error in test file', async ({ runInlineTest }) =
`, `,
}, { 'timeout': 3000 }); }, { 'timeout': 3000 });
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.results).toHaveLength(1);
expect(result.results[0].status).toBe('failed'); expect(result.results[0].status).toBe('failed');
expect(result.results[0].error.message).toContain('Error: worker process exited unexpectedly'); expect(result.results[0].error.message).toContain('Error: worker process exited unexpectedly');
expect(result.results[1].status).toBe('skipped');
}); });
test('fast double SIGINT should be ignored', async ({ interactWithTestRunner }) => { test('fast double SIGINT should be ignored', async ({ interactWithTestRunner }) => {

View file

@ -640,7 +640,7 @@ test('static modifiers should be added in serial mode', async ({ runInlineTest }
}); });
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.passed).toBe(0); expect(result.passed).toBe(0);
expect(result.didNotRun).toBe(3); expect(result.skipped).toBe(3);
expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'slow' }]); expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'slow' }]);
expect(result.report.suites[0].specs[1].tests[0].annotations).toEqual([{ type: 'fixme' }]); expect(result.report.suites[0].specs[1].tests[0].annotations).toEqual([{ type: 'fixme' }]);
expect(result.report.suites[0].specs[2].tests[0].annotations).toEqual([{ type: 'skip' }]); expect(result.report.suites[0].specs[2].tests[0].annotations).toEqual([{ type: 'skip' }]);

View file

@ -47,7 +47,7 @@ test('test.describe.serial should work', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.passed).toBe(2); expect(result.passed).toBe(2);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
expect(result.didNotRun).toBe(2); expect(result.skipped).toBe(2);
expect(result.outputLines).toEqual([ expect(result.outputLines).toEqual([
'test1', 'test1',
'test2', 'test2',
@ -87,7 +87,7 @@ test('test.describe.serial should work in describe', async ({ runInlineTest }) =
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.passed).toBe(2); expect(result.passed).toBe(2);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
expect(result.didNotRun).toBe(2); expect(result.skipped).toBe(2);
expect(result.outputLines).toEqual([ expect(result.outputLines).toEqual([
'test1', 'test1',
'test2', 'test2',
@ -128,7 +128,7 @@ test('test.describe.serial should work with retry', async ({ runInlineTest }) =>
expect(result.passed).toBe(2); expect(result.passed).toBe(2);
expect(result.flaky).toBe(1); expect(result.flaky).toBe(1);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
expect(result.didNotRun).toBe(1); expect(result.skipped).toBe(1);
expect(result.outputLines).toEqual([ expect(result.outputLines).toEqual([
'test1', 'test1',
'test2', 'test2',
@ -272,7 +272,7 @@ test('test.describe.serial should work with test.fail', async ({ runInlineTest }
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.passed).toBe(2); expect(result.passed).toBe(2);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
expect(result.didNotRun).toBe(1); expect(result.skipped).toBe(1);
expect(result.outputLines).toEqual([ expect(result.outputLines).toEqual([
'zero', 'zero',
'one', 'one',
@ -394,55 +394,3 @@ test('test.describe.serial should work with fullyParallel', async ({ runInlineTe
'two', 'two',
]); ]);
}); });
test('serial fail + skip is failed', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test.describe.configure({ mode: 'serial', retries: 1 });
test.describe.serial('serial suite', () => {
test('one', async () => {
expect(test.info().retry).toBe(0);
});
test('two', async () => {
expect(1).toBe(2);
});
test('three', async () => {
});
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(1);
expect(result.passed).toBe(0);
expect(result.skipped).toBe(0);
expect(result.flaky).toBe(1);
expect(result.failed).toBe(1);
expect(result.interrupted).toBe(0);
expect(result.didNotRun).toBe(1);
});
test('serial skip + fail is failed', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test.describe.configure({ mode: 'serial', retries: 1 });
test.describe.serial('serial suite', () => {
test('one', async () => {
expect(test.info().retry).toBe(1);
});
test('two', async () => {
expect(1).toBe(2);
});
test('three', async () => {
});
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(1);
expect(result.passed).toBe(0);
expect(result.skipped).toBe(0);
expect(result.flaky).toBe(1);
expect(result.failed).toBe(1);
expect(result.interrupted).toBe(0);
expect(result.didNotRun).toBe(1);
});

View file

@ -19,7 +19,7 @@ import { test, expect, retries } from './ui-mode-fixtures';
test.describe.configure({ mode: 'parallel', retries }); test.describe.configure({ mode: 'parallel', retries });
test('should merge trace events', async ({ runUITest, server }) => { test('should merge trace events', async ({ runUITest }) => {
const { page } = await runUITest({ const { page } = await runUITest({
'a.test.ts': ` 'a.test.ts': `
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
@ -99,7 +99,7 @@ test('should merge screenshot assertions', async ({ runUITest }, testInfo) => {
]); ]);
}); });
test('should locate sync assertions in source', async ({ runUITest, server }) => { test('should locate sync assertions in source', async ({ runUITest }) => {
const { page } = await runUITest({ const { page } = await runUITest({
'a.test.ts': ` 'a.test.ts': `
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
@ -118,7 +118,7 @@ test('should locate sync assertions in source', async ({ runUITest, server }) =>
).toHaveText('4 expect(1).toBe(1);'); ).toHaveText('4 expect(1).toBe(1);');
}); });
test('should show snapshots for sync assertions', async ({ runUITest, server }) => { test('should show snapshots for sync assertions', async ({ runUITest }) => {
const { page } = await runUITest({ const { page } = await runUITest({
'a.test.ts': ` 'a.test.ts': `
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
@ -150,7 +150,7 @@ test('should show snapshots for sync assertions', async ({ runUITest, server })
).toHaveText('Submit'); ).toHaveText('Submit');
}); });
test('should show image diff', async ({ runUITest, server }) => { test('should show image diff', async ({ runUITest }) => {
const { page } = await runUITest({ const { page } = await runUITest({
'playwright.config.js': ` 'playwright.config.js': `
module.exports = { module.exports = {
@ -175,7 +175,7 @@ test('should show image diff', async ({ runUITest, server }) => {
await expect(page.locator('.image-diff-view .image-wrapper img')).toBeVisible(); await expect(page.locator('.image-diff-view .image-wrapper img')).toBeVisible();
}); });
test('should show screenshot', async ({ runUITest, server }) => { test('should show screenshot', async ({ runUITest }) => {
const { page } = await runUITest({ const { page } = await runUITest({
'playwright.config.js': ` 'playwright.config.js': `
module.exports = { module.exports = {
@ -197,3 +197,32 @@ test('should show screenshot', async ({ runUITest, server }) => {
await expect(page.getByText('Screenshots', { exact: true })).toBeVisible(); await expect(page.getByText('Screenshots', { exact: true })).toBeVisible();
await expect(page.locator('.attachment-item img')).toHaveCount(1); await expect(page.locator('.attachment-item img')).toHaveCount(1);
}); });
test('should not fail on internal page logs', async ({ runUITest, server }) => {
const { page } = await runUITest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('pass', async ({ browser }, testInfo) => {
const context = await browser.newContext({ storageState: { cookies: [], origins: [] } });
const page = await context.newPage();
await page.goto("${server.EMPTY_PAGE}");
await page.context().storageState({ path: testInfo.outputPath('storage.json') });
});
`,
});
await page.getByText('pass').dblclick();
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
await expect(
listItem,
'action list'
).toHaveText([
/Before Hooks[\d.]+m?s/,
/browser.newContext[\d.]+m?s/,
/browserContext.newPage[\d.]+m?s/,
/page.goto/,
/browserContext.storageState[\d.]+m?s/,
/After Hooks/,
]);
});