Compare commits

...

23 commits

Author SHA1 Message Date
Max Schmitt f95d87228a
docs: cherry-pick from ToT (#18521)
List of cherry-picked commits:
- 37cd573652
- 8e9540b7c1
- ce7fc1b9f3
- c4404ea98f
- eb1c92630e
- d4bab139b2
- 2efa96a882
- f6e642e1fa
2022-11-02 16:15:54 -07:00
Playwright Service 63d6a35ce5
chery-pick(#18060): fix(generator): .NET getByRole w/ name (#18066)
This PR cherry-picks the following commits:

- a60073d664

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2022-10-13 19:45:29 +03:00
Yury Semikhatsky fde19c1652
chore: mark 1.27.1 (#18011) 2022-10-11 22:20:07 -07:00
Pavel Feldman 04b3b7190c cherry-pick(#18010): fix(generator): generate nice locators for arbitrary selectors 2022-10-11 17:52:36 -07:00
Yury Semikhatsky 3367ebd968
cherry-pick(#17999): chore: don't fail on undefined video/trace (#18001) 2022-10-11 11:33:33 -07:00
Yury Semikhatsky 730d3226a9
cherry-pick(#17907): docs: improve trace viewer + add video (#18000) 2022-10-11 09:58:32 -07:00
Álvaro Martínez 384b710c24 cherry-pick(#17942): docs(release-notes): add missing reference to Page.getByTestId in 1.27 release notes 2022-10-10 12:36:58 -07:00
Pavel Feldman df7906d92a cherry-pick(#17959): chore: better integrity error message 2022-10-10 12:36:13 -07:00
Pavel Feldman f87d6f2838 cherry-pick(#17961): fix(codegen): use constants when generating C# and Java roles 2022-10-10 12:26:52 -07:00
Playwright Service 4ff2f3d0c5
chery-pick(#17952): fix: fix typo in treeitem role typing (#17965)
This PR cherry-picks the following commits:

- 6b01df6d92

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2022-10-10 22:18:08 +03:00
Playwright Service 75aaaac3eb
chery-pick(#17918): docs: AriaRole is enum (#17921)
This PR cherry-picks the following commits:

- 946994ca92

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2022-10-08 09:03:18 +03:00
Dmitry Gozman af0d2936ac cherry-pick(#17912): docs: mention that exact option is ignored for regex 2022-10-07 10:20:11 -07:00
Dmitry Gozman c05225f9b9
cherry-pick(#17896): chore: make local docker build work on branch (#17906)
Branch does not pass `isDevelopmentMode` check because it does not have
a version ending with `-next`. Therefore, `env.PWTEST_DOCKER_BASE_IMAGE`
is ignored which leads to the pull of non-existent image.
2022-10-07 10:11:41 -07:00
Dmitry Gozman 3f0af1e4df
cherry-pick(#17893): fix(locators): make regex escape work when multiple spaces are present (#17894) 2022-10-06 18:01:14 -07:00
Pavel Feldman 923a583174 cherry-pick(#17888): chore: make role name case-insensitive 2022-10-06 14:36:33 -07:00
Dmitry Gozman b85b1ec6a0 cherry-pick(#17886): docs: v1.27 release notes for python, java and dotnet 2022-10-06 12:53:36 -07:00
Pavel Feldman 299bceaada cherry-pick(#17885): feat(api): make aria roles an enum 2022-10-06 11:35:09 -07:00
Dmitry Gozman 03c7f9c2f0
cherry-pick(#17879): test: rebaseline requestStorageAccess test for firefox-beta (#17884) 2022-10-06 11:02:47 -07:00
Dmitry Gozman 3bf42ce163
cherry-pick(#17864): test: fix "getByText should work" with tracing enabled, docker smoke tests (#17883) 2022-10-06 10:12:22 -07:00
Dmitry Gozman 2993281d78
cherry-pick(#17863): chore(docker): make sure failed commands exit with non-zero code (#17880) 2022-10-06 09:01:08 -07:00
Pavel Feldman 1bc426b94f cherry-pick(#17862): fix(esm+tsconfig): allow mapped ts files in esm mode 2022-10-06 08:08:24 -07:00
Pavel Feldman cfa554972b cherry-pick(#17855): chore: use api selectors in codegen hover 2022-10-06 08:07:45 -07:00
Dmitry Gozman 1150de8e75
chore: mark v1.27.0 (#17860) 2022-10-05 17:00:12 -07:00
70 changed files with 1530 additions and 705 deletions

View file

@ -9,22 +9,22 @@ await locator.click();
``` ```
```java ```java
Locator locator = page.frameLocator("#my-frame").locator("text=Submit"); Locator locator = page.frameLocator("#my-frame").getByText("Submit");
locator.click(); locator.click();
``` ```
```python async ```python async
locator = page.frame_locator("#my-frame").locator("text=Submit") locator = page.frame_locator("#my-frame").get_by_text("Submit")
await locator.click() await locator.click()
``` ```
```python sync ```python sync
locator = page.frame_locator("my-frame").locator("text=Submit") locator = page.frame_locator("my-frame").get_by_text("Submit")
locator.click() locator.click()
``` ```
```csharp ```csharp
var locator = page.FrameLocator("#my-frame").Locator("text=Submit"); var locator = page.FrameLocator("#my-frame").GetByText("Submit");
await locator.ClickAsync(); await locator.ClickAsync();
``` ```

View file

@ -1074,11 +1074,11 @@ Text to locate the element for.
* since: v1.27 * since: v1.27
- `exact` <[boolean]> - `exact` <[boolean]>
Whether to find an exact match: case-sensitive and whole-string. Default to false. Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular expression. Note that exact match still trims whitespace.
## locator-get-by-role-role ## locator-get-by-role-role
* since: v1.27 * since: v1.27
- `role` <[string]> - `role` <[AriaRole]<"alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"|"caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"|"deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"|"group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"|"menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"|"presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|"searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"|"tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem">>
Required aria role. Required aria role.
@ -1178,7 +1178,109 @@ Locate element by the test id. By default, the `data-testid` attribute is used a
## template-locator-get-by-text ## template-locator-get-by-text
Allows locating elements that contain given text. Allows locating elements that contain given text. Consider the following DOM structure:
```html
<div>Hello <span>world</span></div>
<div>Hello</div>
```
You can locate by text substring, exact string, or a regular expression:
```js
// Matches <span>
page.getByText('world')
// Matches first <div>
page.getByText('Hello world')
// Matches second <div>
page.getByText('Hello', { exact: true })
// Matches both <div>s
page.getByText(/Hello/)
// Matches second <div>
page.getByText(/^hello$/i)
```
```python async
# Matches <span>
page.get_by_text("world")
# Matches first <div>
page.get_by_text("Hello world")
# Matches second <div>
page.get_by_text("Hello", exact=True)
# Matches both <div>s
page.get_by_text(re.compile("Hello"))
# Matches second <div>
page.get_by_text(re.compile("^hello$", re.IGNORECASE))
```
```python sync
# Matches <span>
page.get_by_text("world")
# Matches first <div>
page.get_by_text("Hello world")
# Matches second <div>
page.get_by_text("Hello", exact=True)
# Matches both <div>s
page.get_by_text(re.compile("Hello"))
# Matches second <div>
page.get_by_text(re.compile("^hello$", re.IGNORECASE))
```
```java
// Matches <span>
page.getByText("world")
// Matches first <div>
page.getByText("Hello world")
// Matches second <div>
page.getByText("Hello", new Page.GetByTextOptions().setExact(true))
// Matches both <div>s
page.getByText(Pattern.compile("Hello"))
// Matches second <div>
page.getByText(Pattern.compile("^hello$", Pattern.CASE_INSENSITIVE))
```
```csharp
// Matches <span>
page.GetByText("world")
// Matches first <div>
page.GetByText("Hello world")
// Matches second <div>
page.GetByText("Hello", new() { Exact: true })
// Matches both <div>s
page.GetByText(new Regex("Hello"))
// Matches second <div>
page.GetByText(new Regex("^hello$", RegexOptions.IgnoreCase))
```
See also [`method: Locator.filter`] that allows to match by another criteria, like an accessible role, and then filter by the text content.
:::note
Matching by text always normalizes whitespace, even with exact match. For example, it turns multiple spaces into one, turns line breaks into spaces and ignores leading and trailing whitespace.
:::
:::note
Input elements of the type `button` and `submit` are matched by their `value` instead of the text content. For example, locating by text `"Log in"` matches `<input type=button value="Log in">`.
:::
## template-locator-get-by-alt-text ## template-locator-get-by-alt-text

View file

@ -176,7 +176,7 @@ steps:
name: 'Playwright Tests' name: 'Playwright Tests'
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: mcr.microsoft.com/playwright:v1.27.0-focal image: mcr.microsoft.com/playwright:v1.27.1-focal
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
@ -194,7 +194,7 @@ steps:
name: 'Playwright Tests' name: 'Playwright Tests'
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: mcr.microsoft.com/playwright:v1.27.0-focal image: mcr.microsoft.com/playwright:v1.27.1-focal
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Python - name: Set up Python
@ -218,7 +218,7 @@ steps:
name: 'Playwright Tests' name: 'Playwright Tests'
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: mcr.microsoft.com/playwright:v1.27.0-focal image: mcr.microsoft.com/playwright:v1.27.1-focal
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-java@v3 - uses: actions/setup-java@v3
@ -239,7 +239,7 @@ steps:
name: 'Playwright Tests' name: 'Playwright Tests'
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: mcr.microsoft.com/playwright:v1.27.0-focal image: mcr.microsoft.com/playwright:v1.27.1-focal
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Setup dotnet - name: Setup dotnet
@ -264,7 +264,7 @@ steps:
name: 'Playwright Tests - ${{ matrix.project }} - Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }}' name: 'Playwright Tests - ${{ matrix.project }} - Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }}'
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: mcr.microsoft.com/playwright:v1.27.0-focal image: mcr.microsoft.com/playwright:v1.27.1-focal
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -299,7 +299,7 @@ jobs:
- deployment: Run_E2E_Tests - deployment: Run_E2E_Tests
pool: pool:
vmImage: ubuntu-20.04 vmImage: ubuntu-20.04
container: mcr.microsoft.com/playwright:v1.27.0-focal container: mcr.microsoft.com/playwright:v1.27.1-focal
environment: testing environment: testing
strategy: strategy:
runOnce: runOnce:
@ -325,7 +325,7 @@ jobs:
- deployment: Run_E2E_Tests - deployment: Run_E2E_Tests
pool: pool:
vmImage: ubuntu-20.04 vmImage: ubuntu-20.04
container: mcr.microsoft.com/playwright:v1.27.0-focal container: mcr.microsoft.com/playwright:v1.27.1-focal
environment: testing environment: testing
strategy: strategy:
runOnce: runOnce:
@ -368,7 +368,7 @@ Running Playwright on Circle CI is very similar to running on GitHub Actions. In
executors: executors:
pw-focal-development: pw-focal-development:
docker: docker:
- image: mcr.microsoft.com/playwright:v1.27.0-focal - image: mcr.microsoft.com/playwright:v1.27.1-focal
environment: environment:
NODE_ENV: development # Needed if playwright is in `devDependencies` NODE_ENV: development # Needed if playwright is in `devDependencies`
``` ```
@ -404,7 +404,7 @@ to run tests on Jenkins.
```groovy ```groovy
pipeline { pipeline {
agent { docker { image 'mcr.microsoft.com/playwright:v1.27.0-focal' } } agent { docker { image 'mcr.microsoft.com/playwright:v1.27.1-focal' } }
stages { stages {
stage('e2e-tests') { stage('e2e-tests') {
steps { steps {
@ -422,7 +422,7 @@ pipeline {
Bitbucket Pipelines can use public [Docker images as build environments](https://confluence.atlassian.com/bitbucket/use-docker-images-as-build-environments-792298897.html). To run Playwright tests on Bitbucket, use our public Docker image ([see Dockerfile](./docker.md)). Bitbucket Pipelines can use public [Docker images as build environments](https://confluence.atlassian.com/bitbucket/use-docker-images-as-build-environments-792298897.html). To run Playwright tests on Bitbucket, use our public Docker image ([see Dockerfile](./docker.md)).
```yml ```yml
image: mcr.microsoft.com/playwright:v1.27.0-focal image: mcr.microsoft.com/playwright:v1.27.1-focal
``` ```
### GitLab CI ### GitLab CI
@ -435,7 +435,7 @@ stages:
tests: tests:
stage: test stage: test
image: mcr.microsoft.com/playwright:v1.27.0-focal image: mcr.microsoft.com/playwright:v1.27.1-focal
script: script:
... ...
``` ```
@ -451,7 +451,7 @@ stages:
tests: tests:
stage: test stage: test
image: mcr.microsoft.com/playwright:v1.27.0-focal image: mcr.microsoft.com/playwright:v1.27.1-focal
parallel: 7 parallel: 7
script: script:
- npm ci - npm ci
@ -466,7 +466,7 @@ stages:
tests: tests:
stage: test stage: test
image: mcr.microsoft.com/playwright:v1.27.0-focal image: mcr.microsoft.com/playwright:v1.27.1-focal
parallel: parallel:
matrix: matrix:
- PROJECT: ['chromium', 'webkit'] - PROJECT: ['chromium', 'webkit']

View file

@ -13,37 +13,34 @@ Playwright comes with the ability to generate tests out of the box and is a grea
## Running Codegen ## Running Codegen
```bash js ```bash js
npx playwright codegen playwright.dev npx playwright codegen demo.playwright.dev/todomvc
``` ```
```bash java ```bash java
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="codegen playwright.dev" mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="codegen demo.playwright.dev/todomvc"
``` ```
```bash python ```bash python
playwright codegen playwright.dev playwright codegen demo.playwright.dev/todomvc
``` ```
```bash csharp ```bash csharp
pwsh bin/Debug/netX/playwright.ps1 codegen playwright.dev 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 attempt to generate resilient text-based selectors. Run `codegen` and perform actions in the browser. Playwright will generate the code for the user interactions. `Codegen` will attempt to generate resilient text-based selectors.
<img width="1183" alt="Codegen generating code for tests for playwright.dev website" src="https://user-images.githubusercontent.com/13063165/181852815-971c10da-0b55-4e54-8a73-77e1e825193c.png" /> <video width="100%" height="100%" controls muted >
<source src="https://user-images.githubusercontent.com/13063165/197979804-c4fa3347-8fab-4526-a728-c1b2fbd079b4.mp4" type="video/mp4" />
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. Your browser does not support the video tag.
</video>
<img width="1266" alt="Codegen generating code for tests for playwright.dev" src="https://user-images.githubusercontent.com/13063165/183905981-003c4173-0d5e-4960-8190-50e6ca71b2c3.png" />
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.
Use the **clear** button to clear the code to start recording again. Once finished close the Playwright inspector window or stop the terminal command. Use the **clear** button to clear the code to start recording again. Once finished close the Playwright inspector window or stop the terminal command.
To learn more about generating tests check out or detailed guide on [Codegen](./codegen.md). To learn more about generating tests check out or detailed guide on [Codegen](./codegen.md).
## What's Next ## What's Next
- [See a trace of your tests](./trace-viewer-intro.md) - [See a trace of your tests](./trace-viewer-intro.md)

View file

@ -5,28 +5,31 @@ title: "Test Generator"
Playwright comes with the ability to generate tests out of the box and is a great way to quickly get started with testing. It will open two windows, a browser window where you interact with the website you wish to test and the Playwright Inspector window where you can record your tests, copy the tests, clear your tests as well as change the language of your tests. Playwright comes with the ability to generate tests out of the box and is a great way to quickly get started with testing. It will open two windows, a browser window where you interact with the website you wish to test and the Playwright Inspector window where you can record your tests, copy the tests, clear your tests as well as change the language of your tests.
## Running Codegen ## Running Codegen
```bash js ```bash js
npx playwright codegen playwright.dev npx playwright codegen demo.playwright.dev/todomvc
``` ```
```bash java ```bash java
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="codegen playwright.dev" mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="codegen demo.playwright.dev/todomvc"
``` ```
```bash python ```bash python
playwright codegen playwright.dev playwright codegen demo.playwright.dev/todomvc
``` ```
```bash csharp ```bash csharp
pwsh bin/Debug/netX/playwright.ps1 codegen playwright.dev 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 attempt to generate resilient text-based selectors. Run `codegen` and perform actions in the browser. Playwright will generate the code for the user interactions. `Codegen` will attempt to generate resilient text-based selectors.
<img width="1183" alt="Codegen generating code for tests for playwright.dev website" src="https://user-images.githubusercontent.com/13063165/181852815-971c10da-0b55-4e54-8a73-77e1e825193c.png" /> <video width="100%" height="100%" controls muted>
<source src="https://user-images.githubusercontent.com/13063165/197979804-c4fa3347-8fab-4526-a728-c1b2fbd079b4.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
## Emulate viewport size ## Emulate viewport size
@ -72,8 +75,7 @@ playwright codegen --device="iPhone 11" playwright.dev
pwsh bin/Debug/netX/playwright.ps1 codegen --device="iPhone 11" playwright.dev pwsh bin/Debug/netX/playwright.ps1 codegen --device="iPhone 11" playwright.dev
``` ```
<img width="1239" alt="Codegen generating code for tests for playwright.dev website emulated for iPhone 11" src="https://user-images.githubusercontent.com/13063165/182360089-9dc6d33d-480e-4bb2-86a3-fec51c1c228e.png" /> <img width="1254" alt="Codegen generating code for tests for playwright.dev website emulated for iPhone 11" src="https://user-images.githubusercontent.com/13063165/197976789-ee25ed24-69af-4684-b6a4-098673cfb035.png" />
## Emulate color scheme ## Emulate color scheme

View file

@ -14,19 +14,19 @@ This image is published on [Docker Hub].
### Pull the image ### Pull the image
```bash js ```bash js
docker pull mcr.microsoft.com/playwright:v1.27.0-focal docker pull mcr.microsoft.com/playwright:v1.27.1-focal
``` ```
```bash python ```bash python
docker pull mcr.microsoft.com/playwright/python:v1.27.0-focal docker pull mcr.microsoft.com/playwright/python:v1.27.1-focal
``` ```
```bash csharp ```bash csharp
docker pull mcr.microsoft.com/playwright/dotnet:v1.27.0-focal docker pull mcr.microsoft.com/playwright/dotnet:v1.27.1-focal
``` ```
```bash java ```bash java
docker pull mcr.microsoft.com/playwright/java:v1.27.0-focal docker pull mcr.microsoft.com/playwright/java:v1.27.1-focal
``` ```
### Run the image ### Run the image
@ -38,19 +38,19 @@ By default, the Docker image will use the `root` user to run the browsers. This
On trusted websites, you can avoid creating a separate user and use root for it since you trust the code which will run on the browsers. On trusted websites, you can avoid creating a separate user and use root for it since you trust the code which will run on the browsers.
```bash js ```bash js
docker run -it --rm --ipc=host mcr.microsoft.com/playwright:v1.27.0-focal /bin/bash docker run -it --rm --ipc=host mcr.microsoft.com/playwright:v1.27.1-focal /bin/bash
``` ```
```bash python ```bash python
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/python:v1.27.0-focal /bin/bash docker run -it --rm --ipc=host mcr.microsoft.com/playwright/python:v1.27.1-focal /bin/bash
``` ```
```bash csharp ```bash csharp
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/dotnet:v1.27.0-focal /bin/bash docker run -it --rm --ipc=host mcr.microsoft.com/playwright/dotnet:v1.27.1-focal /bin/bash
``` ```
```bash java ```bash java
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/java:v1.27.0-focal /bin/bash docker run -it --rm --ipc=host mcr.microsoft.com/playwright/java:v1.27.1-focal /bin/bash
``` ```
#### Crawling and scraping #### Crawling and scraping
@ -58,19 +58,19 @@ docker run -it --rm --ipc=host mcr.microsoft.com/playwright/java:v1.27.0-focal /
On untrusted websites, it's recommended to use a separate user for launching the browsers in combination with the seccomp profile. Inside the container or if you are using the Docker image as a base image you have to use `adduser` for it. On untrusted websites, it's recommended to use a separate user for launching the browsers in combination with the seccomp profile. Inside the container or if you are using the Docker image as a base image you have to use `adduser` for it.
```bash js ```bash js
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright:v1.27.0-focal /bin/bash docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright:v1.27.1-focal /bin/bash
``` ```
```bash python ```bash python
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/python:v1.27.0-focal /bin/bash docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/python:v1.27.1-focal /bin/bash
``` ```
```bash csharp ```bash csharp
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/dotnet:v1.27.0-focal /bin/bash docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/dotnet:v1.27.1-focal /bin/bash
``` ```
```bash java ```bash java
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/java:v1.27.0-focal /bin/bash docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/java:v1.27.1-focal /bin/bash
``` ```
[`seccomp_profile.json`](https://github.com/microsoft/playwright/blob/main/utils/docker/seccomp_profile.json) is needed to run Chromium with sandbox. This is a [default Docker seccomp profile](https://github.com/docker/engine/blob/d0d99b04cf6e00ed3fc27e81fc3d94e7eda70af3/profiles/seccomp/default.json) with extra user namespace cloning permissions: [`seccomp_profile.json`](https://github.com/microsoft/playwright/blob/main/utils/docker/seccomp_profile.json) is needed to run Chromium with sandbox. This is a [default Docker seccomp profile](https://github.com/docker/engine/blob/d0d99b04cf6e00ed3fc27e81fc3d94e7eda70af3/profiles/seccomp/default.json) with extra user namespace cloning permissions:

View file

@ -11,7 +11,7 @@ Get started by installing Playwright and generating a test to see it in action.
Install the [VS Code extension from the marketplace](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) or from the extensions tab in VS Code. Install the [VS Code extension from the marketplace](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) or from the extensions tab in VS Code.
<img width="1099" alt="VS Code extension for Playwright" src="https://user-images.githubusercontent.com/13063165/188664251-e6e28648-25fb-45bb-98f5-ac6044938475.png" /> <img width="1100" alt="VS Code extension for Playwright" src="https://user-images.githubusercontent.com/13063165/197744119-5ed72385-2037-450b-b988-83b2f7554cf1.png" />
Once installed, open the command panel and type: Once installed, open the command panel and type:
@ -19,39 +19,42 @@ Once installed, open the command panel and type:
Install Playwright Install Playwright
``` ```
<img width="1093" alt="Install Playwright in VS code" src="https://user-images.githubusercontent.com/13063165/188664853-7b3b610b-70ce-4674-ac51-3f2b48dcc589.png" /> <img width="1100" alt="Install Playwright" src="https://user-images.githubusercontent.com/13063165/197744677-edd437e7-15b2-4e3a-8c6b-e728cfe7b65c.png" />
Select **Test: Install Playwright** and Choose the browsers you would like to run your tests on. These can be later configured in the [playwright.config](./test-configuration.md) file. You can also choose if you would like to have a GitHub Actions setup to [run your tests on CI](./ci-intro.md). Select **Test: Install Playwright** and Choose the browsers you would like to run your tests on. These can be later configured in the [playwright.config](./test-configuration.md) file. You can also choose if you would like to have a GitHub Actions setup to [run your tests on CI](./ci-intro.md).
<img width="1093" alt="choose browsers for Playwright in VS Code" src="https://user-images.githubusercontent.com/13063165/188664742-371f2321-67a1-4799-99ba-253a125de838.png" /> <img width="1115" alt="Choose Browsers" src="https://user-images.githubusercontent.com/13063165/197704489-72744c50-81ea-4716-a5f1-52ca801edf1f.png" />
## Running Tests ## Running Tests
You can run a single test by clicking the green triangle next to your test block to run your test. Playwright will run through each line of the test and when it finishes you will see a green tick next to your test block as well as the time it took to run the test. You can run a single test by clicking the green triangle next to your test block to run your test. Playwright will run through each line of the test and when it finishes you will see a green tick next to your test block as well as the time it took to run the test.
<img width="1272" alt="Running Tests in VS Code" src="https://user-images.githubusercontent.com/13063165/188641041-e7f49b0e-758c-4154-b719-b873ba58dca4.png" /> <img width="1114" alt="Run a single test" src="https://user-images.githubusercontent.com/13063165/197712138-f4593c0d-ec7e-4a61-b2cd-59fc2af39c6a.png" />
### Run Tests and Show Browsers ### Run Tests and Show Browsers
You can also run your tests and show the browsers by selecting the option **Show Browsers** in the testing sidebar. Then when you click the green triangle to run your test the browser will open and you will visually see it run through your test. Leave this selected if you want browsers open for all your tests or uncheck it if you prefer your tests to run in headless mode with no browser open. You can also run your tests and show the browsers by selecting the option **Show Browsers** in the testing sidebar. Then when you click the green triangle to run your test the browser will open and you will visually see it run through your test. Leave this selected if you want browsers open for all your tests or uncheck it if you prefer your tests to run in headless mode with no browser open.
<img width="1394" alt="Run Tests and Show Browsers in VS Code" src="https://user-images.githubusercontent.com/13063165/188662739-5b191b2d-7055-4f33-9399-bc8626163293.png" /> <img width="1350" alt="Show browsers while running tests" src="https://user-images.githubusercontent.com/13063165/197714311-1d8c0955-9c5b-44ec-b429-160fa3d6b7a4.png" />
Use the **Close all browsers** button to close all browsers. Use the **Close all browsers** button to close all browsers.
<img width="1272" alt="Close Browsers in VS Code" src="https://user-images.githubusercontent.com/13063165/188663381-c0293d02-75f9-46d4-852f-43aebe508d4a.png" />
### View and Run All Tests ### View and Run All Tests
View all tests in the testing sidebar and extend the tests by clicking on each test. Tests that have not been run will not have the green check next to them. Run all tests by clicking on the white triangle as you hover over the tests in the testing sidebar. View all tests in the testing sidebar and extend the tests by clicking on each test. Tests that have not been run will not have the green check next to them. Run all tests by clicking on the white triangle as you hover over the tests in the testing sidebar.
<img width="1272" alt="View and Run All Tests in VS Code" src="https://user-images.githubusercontent.com/13063165/188641364-3bfa74f8-2e8a-45e5-92e1-4cbee0660e8a.png" /> <img width="1114" alt="Run all tests in file" src="https://user-images.githubusercontent.com/13063165/197712455-496f5300-79ed-4eae-9cc1-52cc9f3c019b.png" />
### Run Tests on Specific Browsers ### Run Tests on Specific Browsers
The VS Code test runner runs your tests on the default browser of Chrome. To run on other/multiple browsers click the play button's dropdown and choose the option of "Select Default Profile" and select the browsers you wish to run your tests on. The VS Code test runner runs your tests on the default browser of Chrome. To run on other/multiple browsers click the play button's dropdown and choose another profile or modify the default profile by clicking **Select Default Profile** and select the browsers you wish to run your tests on.
<img width="1272" alt="Run Tests on Specific Browsers in VS Code" src="https://user-images.githubusercontent.com/13063165/188642000-f3c59179-8b44-40cb-a573-c2d9965737a6.png" /> <img width="1116" alt="selecting browsers" src="https://user-images.githubusercontent.com/13063165/197728519-5381efc0-30d4-490e-82a8-e43eb35daf9f.png" />
Choose various or all profiles to run tests on multiple profiles. These profiles are read from the [playwright.config](./test-configuration.md) file. To add more profiles such as a mobile profile, first add it to your config file and it will then be available here.
<img width="1012" alt="choosing default profiles" src="https://user-images.githubusercontent.com/13063165/197710323-ec752f91-86c5-45c8-81b3-eac2e8ed0bfb.png" />
## Debugging Tests ## Debugging Tests
@ -61,13 +64,13 @@ With the VS Code extension you can debug your tests right in VS Code see error m
If your test fails VS Code will show you error messages right in the editor showing what was expected, what was received as well as a complete call log. If your test fails VS Code will show you error messages right in the editor showing what was expected, what was received as well as a complete call log.
<img width="1272" alt="Error Messages in VS Code" src="https://user-images.githubusercontent.com/13063165/188642424-37da9e6c-b24a-4755-b14c-ceefa59483d2.png" /> <img width="1339" alt="error messaging in vs code" src="https://user-images.githubusercontent.com/13063165/197967016-b4c35689-0c04-4ea3-a288-35b98056efec.png" />
### Run in Debug Mode ### Run in Debug Mode
To set a breakpoint click next to the line number where you want the breakpoint to be until a red dot appears. Run the tests in debug mode by right clicking on the line next to the test you want to run. A browser window will open and the test will run and pause at where the breakpoint is set. To set a breakpoint click next to the line number where you want the breakpoint to be until a red dot appears. Run the tests in debug mode by right clicking on the line next to the test you want to run. A browser window will open and the test will run and pause at where the breakpoint is set.
<img width="1272" alt="Run tests in Debug Mode in VS Code" src="https://user-images.githubusercontent.com/13063165/188642947-48f4eeaa-486d-4657-9819-63ad742ee7e2.png" /> <img width="1149" alt="setting debug test mode" src="https://user-images.githubusercontent.com/13063165/197715919-98f32957-2ae1-478b-9588-d93cc4548c67.png" />
### Live Debugging ### Live Debugging
@ -75,37 +78,42 @@ To set a breakpoint click next to the line number where you want the breakpoint
You can modify your test right in VS Code while debugging and Playwright will highlight the selector in the browser. This is a great way of seeing if the selector exits or if there is more than one result. You can step through the tests, pause the test and rerun the tests from the menu in VS Code. You can modify your test right in VS Code while debugging and Playwright will highlight the selector in the browser. This is a great way of seeing if the selector exits or if there is more than one result. You can step through the tests, pause the test and rerun the tests from the menu in VS Code.
<img width="1394" alt="Live debugging in VS Code" src="https://user-images.githubusercontent.com/13063165/188644314-89967ab8-2415-4e55-bbca-b3840d347ca4.png" /> <img width="1350" alt="Live Debugging in VS Code" src="https://user-images.githubusercontent.com/13063165/197967885-512df81f-12e3-45e5-b90f-42ed0f064eac.png" />
### Debug in different Browsers
Debug your tests on specific browsers by selecting a profile from the dropdown. Set the default profile or select more than one profile to debug various profiles. Playwright will launch the first profile and once finished debugging it will then launch the next one.
<img width="1221" alt="debugging on specific profile" src="https://user-images.githubusercontent.com/13063165/197738552-06aa8a83-6a6b-4aad-ab23-d449640e1f5f.png" />
To learn more about debugging, see [Debugging in Visual Studio Code](https://code.visualstudio.com/docs/editor/debugging).
## Generating Tests ## Generating Tests
CodeGen will auto generate your tests for you as you perform actions in the browser and is a great way to quickly get started. The viewport for the browser window is set to a specific width and height. See the [configuration guide](./test-configuration.md) to change the viewport or emulate different environments. CodeGen will auto generate your tests for you as you perform actions in the browser and is a great way to quickly get started. The viewport for the browser window is set to a specific width and height. See the [configuration guide](./test-configuration.md) to change the viewport or emulate different environments.
### 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 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.
<img width="1272" alt="Recording a Test in VS Code" src="https://user-images.githubusercontent.com/13063165/188644755-2ab9c826-79a9-4c52-8963-26bb9e853170.png" /> <video width="100%" height="100%" controls muted>
<source src="https://user-images.githubusercontent.com/13063165/197721416-e525dd60-51a6-4740-ad8b-0f56f4d20045.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
### Record a Test Starting From Another Test ### Record From Here
Use the **Record from here** button to record a test from a specific line in your test file. This will open up a browser window and record the test from the line you selected. A new test file will now be created with the name `test-2.spec.ts` and will include the test code up to the selected line of the test file where you ran the **Record from here** button. You can then continue to generate the new test by clicking around in the browser window. Record a new test snippet. This creates a new empty test file but the recording starts from the current browser state from the previous test instead of starting a new browser. This snippet can then be pasted into a previous test file so it can be properly run. Note in the example below the test starts from the last state of a previous test and therefore has no `page.goto()` action.
<img width="1272" alt="Record a test from here in VS Code" src="https://user-images.githubusercontent.com/13063165/188654397-dc6e8677-e957-48ca-906e-8dd38da97c3b.png" />
### Selector Highlighting <img width="1392" alt="record a test from a specific browser state" src="https://user-images.githubusercontent.com/13063165/197740755-fa845cbb-6292-44a4-8134-af1ce15f438a.png" />
As you interact with the page Codegen will generate the test for you in the newly created file in VS Code. When you hover over an element Playwright will highlight the element and show the [selector](./selectors.md) underneath it.
<img width="1394" alt="Selector Highlighting in VS Code" src="https://user-images.githubusercontent.com/13063165/188645469-cd9e925a-fb75-4250-bbdd-f14f2338ba34.png" />
### Picking a Selector ### Picking a Selector
Pick a selector and copy it into your test file by clicking the **Pick selector** button form the testing sidebar. Then in the browser click the selector you require and it will now show up in the **Pick selector** box in VS Code. Press 'enter' on your keyboard to copy the selector into the clipboard and then paste anywhere in your code. Or press 'escape' if you want to cancel. Pick a selector and copy it into your test file by clicking the **Pick selector** button form the testing sidebar. Then in the browser click the selector you require and it will now show up in the **Pick selector** box in VS Code. Press 'enter' on your keyboard to copy the selector into the clipboard and then paste anywhere in your code. Or press 'escape' if you want to cancel.
<img width="1394" alt="Selector Highlighting in VS Code" src="https://user-images.githubusercontent.com/13063165/188645977-2d5d1a50-d0f0-4d2e-ba30-59899bd3c77c.png" /> <img width="1394" alt="Pick selectors" src="https://user-images.githubusercontent.com/13063165/197714946-cb82231d-a6f8-4183-b54b-3375ffaa7092.png" />

View file

@ -253,3 +253,79 @@ unless page navigates or the handle is manually disposed via the [`method: JSHan
- [`method: Page.evaluateHandle`] - [`method: Page.evaluateHandle`]
- [`method: Page.querySelector`] - [`method: Page.querySelector`]
- [`method: Page.querySelectorAll`] - [`method: Page.querySelectorAll`]
## Locator vs ElementHandle
:::caution
We only recommend using [ElementHandle] in the rare cases when you need to perform extensive DOM traversal
on a static page. For all user actions and assertions use locator instead.
:::
The difference between the [Locator] and [ElementHandle] is that the latter points to a particular element, while Locator captures the logic of how to retrieve that element.
In the example below, handle points to a particular DOM element on page. If that element changes text or is used by React to render an entirely different component, handle is still pointing to that very stale DOM element. This can lead to unexpected behaviors.
```js
const handle = await page.$('text=Submit');
// ...
await handle.hover();
await handle.click();
```
```java
ElementHandle handle = page.querySelector("text=Submit");
handle.hover();
handle.click();
```
```python async
handle = await page.query_selector("text=Submit")
await handle.hover()
await handle.click()
```
```python sync
handle = page.query_selector("text=Submit")
handle.hover()
handle.click()
```
```csharp
var handle = await page.QuerySelectorAsync("text=Submit");
await handle.HoverAsync();
await handle.ClickAsync();
```
With the locator, every time the locator is used, up-to-date DOM element is located in the page using the selector. So in the snippet below, underlying DOM element is going to be located twice.
```js
const locator = page.getByText('Submit');
// ...
await locator.hover();
await locator.click();
```
```java
Locator locator = page.getByText("Submit");
locator.hover();
locator.click();
```
```python async
locator = page.get_by_text("Submit")
await locator.hover()
await locator.click()
```
```python sync
locator = page.get_by_text("Submit")
locator.hover()
locator.click()
```
```csharp
var locator = page.GetByText("Submit");
await locator.HoverAsync();
await locator.ClickAsync();
```

View file

@ -4,31 +4,68 @@ title: "Locators"
--- ---
[Locator]s are the central piece of Playwright's auto-waiting and retry-ability. In a nutshell, locators represent [Locator]s are the central piece of Playwright's auto-waiting and retry-ability. In a nutshell, locators represent
a way to find element(s) on the page at any moment. Locator can be created with the [`method: Page.locator`] method. a way to find element(s) on the page at any moment.
### Quick Guide
These are the recommended built in locators.
- [`method: Page.getByRole`] to locate by explicit and implicit accessibility attributes.
- [`method: Page.getByText`] to locate by text content.
- [`method: Page.getByLabel`] to locate a form control by associated label's text.
- [`method: Page.getByPlaceholder`] to locate an input by placeholder.
- [`method: Page.getByAltText`] to locate an element, usually image, by its text alternative.
- [`method: Page.getByTitle`] to locate an element by its title.
- [`method: Page.getByTestId`] to locate an element based on its `data-testid` attribute (other attribute can be configured).
```js ```js
const locator = page.getByText('Submit'); await page.getByLabel('User Name').fill('John');
await locator.click();
await page.getByLabel('Password').fill('secret-password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Welcome, John!')).toBeVisible();
``` ```
```java ```java
Locator locator = page.getByText("Submit"); page.getByLabel("User Name").fill("John");
locator.click();
page.getByLabel("Password").fill("secret-password");
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")).click();
assertThat(page.getByText("Welcome, John!")).isVisible();
``` ```
```python async ```python async
locator = page.get_by_text("Submit") await page.get_by_label("User Name").fill("John")
await locator.click()
await page.get_by_label("Password").fill("secret-password")
await page.get_by_role("button", name="Sign in").click()
await expect(page.get_by_text("Welcome, John!")).to_be_visible()
``` ```
```python sync ```python sync
locator = page.get_by_text("Submit") page.get_by_label("User Name").fill("John")
locator.click()
page.get_by_label("Password").fill("secret-password")
page.get_by_role("button", name="Sign in").click()
expect(page.get_by_text("Welcome, John!")).to_be_visible()
``` ```
```csharp ```csharp
var locator = page.GetByText("Submit"); await page.GetByLabel("User Name").FillAsync("John");
await locator.ClickAsync();
await page.GetByLabel("Password").FillAsync("secret-password");
await page.GetByRole("button", new() { Name = "Sign in" }).ClickAsync();
await Expect(page.GetByText("Welcome, John!")).ToBeVisibleAsync();
``` ```
Every time locator is used for some action, up-to-date DOM element is located in the page. So in the snippet Every time locator is used for some action, up-to-date DOM element is located in the page. So in the snippet
@ -71,75 +108,57 @@ await locator.ClickAsync();
Locators are strict. This means that all operations on locators that imply Locators are strict. This means that all operations on locators that imply
some target DOM element will throw an exception if more than one element matches some target DOM element will throw an exception if more than one element matches
given selector. given selector. For example, the following call throws if there are several buttons in the DOM:
```js ```js
// Throws if there are several buttons in DOM:
await page.getByRole('button').click(); await page.getByRole('button').click();
```
// Works because we explicitly tell locator to pick the first element: ```python async
await page.getByRole('button').first().click(); // ⚠️ using first disables strictness await page.get_by_role("button").click()
```
// Works because count knows what to do with multiple matches: ```python sync
page.get_by_role("button").click()
```
```java
page.getByRole("button").click();
```
```csharp
await page.GetByRole("button").ClickAsync();
```
On the other hand, Playwright understands when you perform a multiple-element operation,
so the following call works perfectly fine when locator resolves to multiple elements.
```js
await page.getByRole('button').count(); await page.getByRole('button').count();
``` ```
```python async ```python async
# Throws if there are several buttons in DOM:
await page.get_by_role("button").click()
# Works because we explicitly tell locator to pick the first element:
await page.get_by_role("button").first.click() # ⚠️ using first disables strictness
# Works because count knows what to do with multiple matches:
await page.get_by_role("button").count() await page.get_by_role("button").count()
``` ```
```python sync ```python sync
# Throws if there are several buttons in DOM:
page.get_by_role("button").click()
# Works because we explicitly tell locator to pick the first element:
page.get_by_role("button").first.click() # ⚠️ using first disables strictness
# Works because count knows what to do with multiple matches:
page.get_by_role("button").count() page.get_by_role("button").count()
``` ```
```java ```java
// Throws if there are several buttons in DOM:
page.getByRole("button").click();
// Works because we explicitly tell locator to pick the first element:
page.getByRole("button").first().click(); // ⚠️ using first disables strictness
// Works because count knows what to do with multiple matches:
page.getByRole("button").count(); page.getByRole("button").count();
``` ```
```csharp ```csharp
// Throws if there are several buttons in DOM:
await page.GetByRole("button").ClickAsync();
// Works because we explicitly tell locator to pick the first element:
await page.GetByRole("button").First.ClickAsync(); // ⚠️ using First disables strictness
// Works because Count knows what to do with multiple matches:
await page.GetByRole("button").CountAsync(); await page.GetByRole("button").CountAsync();
``` ```
:::caution You can explicitly opt-out from strictness check by telling Playwright which element to use when multiple element match, through [`method: Locator.first`], [`method: Locator.last`], and [`method: Locator.nth`]. These methods are **not recommended** because when your page changes, Playwright may click on an element you did not intend. Instead, follow best practices below to create a locator that uniquely identifies the target element.
Using [`method: Locator.first`], [`method: Locator.last`], and [`method: Locator.nth`] is discouraged since it disables the concept of strictness, and as your page changes, Playwright may click on an element you did not intend. It's better to make your locator more specific.
:::
## Locating elements ## Locating elements
Use [`method: Page.locator`] method to create a locator. This method takes a selector that describes how to find an element in the page. The choice of selectors determines the resiliency of the test when the underlying web page changes. To reduce the maintenance burden, we recommend prioritizing user-facing attributes and explicit contracts. Playwright comes with multiple built-in ways to create a locator. To make tests resilient, we recommend prioritizing user-facing attributes and explicit contracts, and provide dedicated methods for them, such as [`method: Page.getByText`]. It is often convenient to use the [code generator](./codegen.md) to generate a locator, and then edit it as you'd like.
### Locate by text content using `text=`
The easiest way to find an element is to look for the text it contains.
```js ```js
await page.getByText('Log in').click(); await page.getByText('Log in').click();
@ -157,29 +176,38 @@ page.get_by_text("Log in").click()
await page.GetByText("Log in").ClickAsync(); await page.GetByText("Log in").ClickAsync();
``` ```
You can also [filter by text](#filter-by-text) when locating in some other way, for example find a particular item in the list. If you absolutely must use CSS or XPath locators, you can use [`method: Page.locator`] to create a locator that takes a [selector](./selectors.md) describing how to find an element in the page.
Note that all methods that create a locator, such as [`method: Page.getByLabel`], are also available on the [Locator] and [FrameLocator] classes, so you can chain them and iteratively narrow down your locator.
```js ```js
await page.locator('data-test-id=product-item', { hasText: 'Playwright Book' }).click(); const locator = page.frameLocator('#my-frame').getByText('Submit');
await locator.click();
``` ```
```java ```java
page.locator("data-test-id=product-item", new Page.LocatorOptions().setHasText("Playwright Book")).click(); Locator locator = page.frameLocator("#my-frame").getByText("Submit");
locator.click();
``` ```
```python async ```python async
await page.locator("data-test-id=product-item", has_text="Playwright Book").click() locator = page.frame_locator("#my-frame").get_by_text("Submit")
await locator.click()
``` ```
```python sync ```python sync
page.locator("data-test-id=product-item", has_text="Playwright Book").click() locator = page.frame_locator("my-frame").get_by_text("Submit")
locator.click()
``` ```
```csharp ```csharp
await page.Locator("data-test-id=product-item", new() { HasText = "Playwright Book" }).ClickAsync(); var locator = page.FrameLocator("#my-frame").GetByText("Submit");
await locator.ClickAsync();
``` ```
[Learn more about the `text` selector](./selectors.md#text-selector). ### Locate based on accessible attributes
### Locate based on accessible attributes using `role=` The [`method: Page.getByRole`] locator reflects how users and assistive technology perceive the page, for example whether some element is a button or a checkbox. When locating by role, you should usually pass the accessible name as well, so that locator pinpoints the exact element.
The `role` selector reflects how users and assistive technology percieve the page, for example whether some element is a button or a checkbox. When locating by role, you should usually pass the accessible name as well, so that locator pinpoints the exact element.
```js ```js
await page.getByRole('button', { name: /submit/i }).click(); await page.getByRole('button', { name: /submit/i }).click();
@ -188,113 +216,255 @@ await page.getByRole('checkbox', { checked: true, name: "Check me" }).check();
``` ```
```python async ```python async
await page.get_by_role("button", name=re.compile("(?i)submit")).click() await page.get_by_role("button", name=re.compile("submit", re.IGNORECASE)).click()
await page.get_by_role("checkbox", checked=True, name="Check me"]).check() await page.get_by_role("checkbox", checked=True, name="Check me").check()
``` ```
```python sync ```python sync
page.get_by_role("button", name=re.compile("(?i)submit")).click() page.get_by_role("button", name=re.compile("submit", re.IGNORECASE)).click()
page.get_by_role("checkbox", checked=True, name="Check me"]).check() page.get_by_role("checkbox", checked=True, name="Check me").check()
``` ```
```java ```java
page.getByRole("button", new Page.GetByRoleOptions().setName(Pattern.compile("(?i)submit"))).click(); page.getByRole("button", new Page.GetByRoleOptions().setName(Pattern.compile("submit", Pattern.CASE_INSENSITIVE))).click();
page.getByRole("checkbox", new Page.GetByRoleOptions().setChecked(true).setName("Check me"))).check(); page.getByRole("checkbox", new Page.GetByRoleOptions().setChecked(true).setName("Check me"))).check();
``` ```
```csharp ```csharp
await page.GetByRole("button", new() { Name = new Regex("(?i)submit") }).ClickAsync(); await page.GetByRole("button", new() { Name = new Regex("submit", RegexOptions.IgnoreCase) }).ClickAsync();
await page.GetByRole("checkbox", new() { Checked = true, Name = "Check me" }).CheckAsync(); await page.GetByRole("checkbox", new() { Checked = true, Name = "Check me" }).CheckAsync();
``` ```
[Learn more about the `role` selector](./selectors.md#role-selector). Role locators follow W3C specifications for [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles), [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
### Define explicit contract and use `data-test-id=` Note that role locators **do not replace** accessibility audits and conformance tests, but rather give early feedback about the ARIA guidelines.
User-facing attributes like text or accessible name can change frequently. In this case it is convenient to define explicit test ids, for example with a `data-test-id` attribute. Playwright has dedicated support for `id`, `data-test-id`, `data-test` and `data-testid` attributes.
```html
<button data-test-id="directions">Itinéraire</button>
```
```js
await page.locator('data-test-id=directions').click();
```
```java
page.locator("data-test-id=directions").click();
```
```python async
await page.locator('data-test-id=directions').click()
```
```python sync
page.locator('data-test-id=directions').click()
```
```csharp
await page.Locator("data-test-id=directions").ClickAsync();
```
### Locate by label text ### Locate by label text
Most form controls usually have dedicated labels that could be conveniently used to interact with the form. Input actions in Playwright automatically distinguish between labels and controls, so you can just locate the label to perform an action on the associated control. Most form controls usually have dedicated labels that could be conveniently used to interact with the form. In this case, you can locate the control by its associated label using [`method: Page.getByLabel`].
For example, consider the following DOM structure. For example, consider the following DOM structure.
```html ```html
<label for="password">Password:</label><input type="password"> <label for="password">Password:</label><input type="password" id="password">
``` ```
You can target the label with something like `text=Password` and perform the following actions on the password input: You can fill the input after locating it by the label text:
- `click` will click the label and automatically focus the input field;
- `fill` will fill the input field;
- `inputValue` will return the value of the input field;
- `selectText` will select text in the input field;
- `setInputFiles` will set files for the input field with `type=file`;
- `selectOption` will select an option from the select box.
For example, to fill the input by targeting the label:
```js ```js
await page.getByText('Password').fill('secret'); await page.getByLabel('Password').fill('secret');
``` ```
```java ```java
page.getByText("Password").fill("secret"); page.getByLabel("Password").fill("secret");
``` ```
```python async ```python async
await page.get_by_text('Password').fill('secret') await page.get_by_label("Password").fill("secret")
``` ```
```python sync ```python sync
page.get_by_text('Password').fill('secret') page.get_by_label("Password").fill("secret")
``` ```
```csharp ```csharp
await page.GetByText("Password").FillAsync("secret"); await page.GetByLabel("Password").FillAsync("secret");
``` ```
However, other methods will target the label itself, for example `textContent` will return the text content of the label, not the input field. ### Locate by placeholder text
Inputs may have a placeholder attribute to hint to the user what value should be entered. You can locate such an input using [`method: Page.getByPlaceholder`].
For example, consider the following DOM structure.
```html
<input id="email" name="email" type="email" placeholder="name@example.com">
```
You can fill the input after locating it by the placeholder text:
```js
await page.getByPlaceholder("name@example.com").fill("playwright@microsoft.com");
```
```java
page.getByPlaceholder("name@example.com").fill("playwright@microsoft.com");
```
```python async
await page.get_by_placeholder("name@example.com").fill("playwright@microsoft.com")
```
```python sync
page.get_by_placeholder("name@example.com").fill("playwright@microsoft.com")
```
```csharp
await page.GetByPlacheolder("name@example.com").FillAsync("playwright@microsoft.com");
```
### Locate by text
The easiest way to find an element is to look for the text it contains. You can match by a substring, exact string, or a regular expression when using [`method: Page.getByText`].
```js
await page.getByText('Log in').click();
await page.getByText('Log in', { exact: true }).click();
await page.getByText(/log in$/i).click();
```
```java
page.getByText("Log in").click();
page.getByText("Log in", new Page.GetByTextOptions().setExact(true)).click();
page.getByText(Pattern.compile("log in$", Pattern.CASE_INSENSITIVE)).click();
```
```python async
await page.get_by_text("Log in").click()
await page.get_by_text("Log in", exact=True).click()
await page.get_by_text(re.compile("Log in", re.IGNORECASE)).click()
```
```python sync
page.get_by_text("Log in").click()
page.get_by_text("Log in", exact=True).click()
page.get_by_text(re.compile("Log in", re.IGNORECASE)).click()
```
```csharp
await page.GetByText("Log in").ClickAsync();
await page.GetByText("Log in", new() { Exact: true }).ClickAsync();
await page.GetByText(new Regex("Log in", RegexOptions.IgnoreCase)).ClickAsync();
```
You can also [filter by text](#filter-by-text) when locating in some other way, for example find a particular item in the list.
```js
await page.getByTestId('product-item').filter({ hasText: 'Playwright Book' }).click();
```
```java
page.getByTestId("product-item").filter(new Locator.FilterOptions().setHasText("Playwright Book")).click();
```
```python async
await page.get_by_test_id("product-item").filter(has_text="Playwright Book").click()
```
```python sync
page.get_by_test_id("product-item").filter(has_text="Playwright Book").click()
```
```csharp
await page.GetByTestId("product-item").Filter(new() { HasText = "Playwright Book" }).ClickAsync();
```
:::note
Matching by text always normalizes whitespace, even with exact match. For example, it turns multiple spaces into one, turns line breaks into spaces and ignores leading and trailing whitespace.
:::
### Locate by alt text
All images should have an `alt` attribute that describes the image. You can locate an image based on the text alternative using [`method: Page.getByAltText`].
For example, consider the following DOM structure.
```html
<img alt="playwright logo" src="/playwright-logo.png" />
```
You can click on the image after locating it by the text alternative:
```js
await page.getByAltText('playwright logo').click();
```
```java
page.getByAltText("playwright logo").click();
```
```python async
await page.get_by_alt_text("playwright logo").click()
```
```python sync
page.get_by_alt_text("playwright logo").click()
```
```csharp
await page.GetByAltText("playwright logo").ClickAsync();
```
### Locate by title
Locate an element with a matching title attribute using [`method: Page.getByTitle`].
For example, consider the following DOM structure.
```html
<span title='Issues count'>25 issues</span>
```
You can check the issues count after locating it by the title text:
```js
await expect(page.getByTitle('Issues count')).toHaveText('25 issues');
```
```java
assertThat(page.getByTitle("Issues count")).hasText("25 issues");
```
```python async
await expect(page.get_by_title("Issues count")).to_have_text("25 issues")
```
```python sync
expect(page.get_by_title("Issues count")).to_have_text("25 issues")
```
```csharp
await Expect(page.GetByTitle("Issues count")).toHaveText("25 issues");
```
### Define explicit contract and use a data-testid attribute
User-facing attributes like text or accessible name can change over time. In this case it is convenient to define explicit test ids and query them with [`method: Page.getByTestId`].
```html
<button data-testid="directions">Itinéraire</button>
```
```js
await page.getByTestId('directions').click();
```
```java
page.getByTestId("directions").click();
```
```python async
await page.get_by_test_id("directions").click()
```
```python sync
page.get_by_test_id("directions").click()
```
```csharp
await page.GetByTestId("directions").ClickAsync();
```
By default, [`method: Page.getByTestId`] will locate elements based on the `data-testid` attribute, but you can configure it in your test config or calling [`method: Selectors.setTestIdAttribute`].
### Locate in a subtree ### Locate in a subtree
You can chain [`method: Page.locator`] and [`method: Locator.locator`] calls to narrow down the search to a particular part of the page. You can chain methods that create a locator, like [`method: Page.getByText`] or [`method: Locator.getByRole`], to narrow down the search to a particular part of the page.
For example, consider the following DOM structure: For example, consider the following DOM structure:
```html ```html
<div data-test-id='product-card'> <div data-testid='product-card'>
<span>Product 1</span> <span>Product 1</span>
<button>Buy</button> <button>Buy</button>
</div> </div>
<div data-test-id='product-card'> <div data-testid='product-card'>
<span>Product 2</span> <span>Product 2</span>
<button>Buy</button> <button>Buy</button>
</div> </div>
@ -303,38 +473,38 @@ For example, consider the following DOM structure:
For example, we can first find a product card that contains text "Product 2", and then click the button in this specific product card. For example, we can first find a product card that contains text "Product 2", and then click the button in this specific product card.
```js ```js
const product = page.locator('data-test-id=product-card', { hasText: 'Product 2' }); const product = page.getByTestId('product-card').filter({ hasText: 'Product 2' });
await product.getByText('Buy').click(); await product.getByText('Buy').click();
``` ```
```python async ```python async
product = page.locator("data-test-id=product-card", has_text="Product 2") product = page.get_by_test_id("product-card").filter(has_text="Product 2")
await product.getByText("Buy").click() await product.getByText("Buy").click()
``` ```
```python sync ```python sync
product = page.locator("data-test-id=product-card", has_text="Product 2") product = page.get_by_test_id("product-card").filter(has_text="Product 2")
product.get_by_text("Buy").click() product.get_by_text("Buy").click()
``` ```
```java ```java
Locator product = page.locator("data-test-id=product-card", new Page.LocatorOptions().setHasText("Product 2")); Locator product = page.getByTestId("product-card").filter(new Locator.FilterOptions().setHasText("Product 2"));
product.get_by_text("Buy").click(); product.get_by_text("Buy").click();
``` ```
```csharp ```csharp
var product = page.Locator("data-test-id=product-card", new() { HasText = "Product 2" }); var product = page.GetByTestId("product-card").Filter(new() { HasText = "Product 2" });
await product.GetByText("Buy").clickAsync(); await product.GetByText("Buy").clickAsync();
``` ```
### Locate by CSS or XPath selector ### Locate by CSS or XPath selector
Playwright supports CSS and XPath selectors, and auto-detects them if you omit `css=` or `xpath=` prefix: Playwright supports CSS and XPath selectors, and auto-detects them if you omit `css=` or `xpath=` prefix. Use [`method: Page.locator`] for this:
```js ```js
await page.locator('css=button').click(); await page.locator('css=button').click();
@ -376,11 +546,7 @@ await page.Locator('button').ClickAsync();
await page.Locator('//button').ClickAsync(); await page.Locator('//button').ClickAsync();
``` ```
### Avoid locators tied to implementation XPath and CSS selectors can be tied to the DOM structure or implementation. These selectors can break when the DOM structure changes. Long CSS or XPath chains below are an example of a **bad practice** that leads to unstable tests:
XPath and CSS selectors can be tied to the DOM structure or implementation. These selectors can break when the DOM structure changes. Similarly, [`method: Locator.nth`], [`method: Locator.first`], and [`method: Locator.last`] are tied to implementation and the structure of the DOM, and will target the incorrect element if the DOM changes.
Long CSS or XPath chains below are an example of a **bad practice** that leads to unstable tests:
```js ```js
await page.locator('#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input').click(); await page.locator('#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input').click();
@ -412,7 +578,7 @@ await page.Locator("#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > di
await page.Locator("//*[@id='tsf']/div[2]/div[1]/div[1]/div/div[2]/input").ClickAsync(); await page.Locator("//*[@id='tsf']/div[2]/div[1]/div[1]/div/div[2]/input").ClickAsync();
``` ```
Instead, try to come up with a locator that is close to how user perceives the page or [define an explicit testing contract](#define-explicit-contract-and-use-data-test-id). Instead, try to come up with a locator that is close to how user perceives the page or [define an explicit testing contract](#define-explicit-contract-and-use-pagegetbytestidtestid).
### Locate elements that contain other elements ### Locate elements that contain other elements
@ -421,24 +587,24 @@ Instead, try to come up with a locator that is close to how user perceives the p
Locator can be optionally filtered by text. It will search for a particular string somewhere inside the element, possibly in a descendant element, case-insensitively. You can also pass a regular expression. Locator can be optionally filtered by text. It will search for a particular string somewhere inside the element, possibly in a descendant element, case-insensitively. You can also pass a regular expression.
```js ```js
await page.locator('button', { hasText: 'Click me' }).click(); await page.getByTestId('product-card').filter({ hasText: 'Product 3' }).click();
await page.locator('button', { hasText: /Click me/ }).click(); await page.getByTestId('product-card').filter({ hasText: /product 3/ }).click();
``` ```
```java ```java
page.locator("button", new Page.LocatorOptions().setHasText("Click me")).click(); page.getByTestId("product-card").filter(new Locator.FilterOptions().setHasText("Product 3")).click();
page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("Click me"))).click(); page.getByTestId("product-card").filter(new Locator.FilterOptions().setHasText(Pattern.compile("Product 3"))).click();
``` ```
```python async ```python async
await page.locator("button", has_text="Click me").click() await page.get_by_test_id("product-card").filter(has_text="Product 3").click()
await page.locator("button", has_text=re.compile("Click me")).click() await page.get_by_test_id("product-card").filter(has_text=re.compile("Product 3")).click()
``` ```
```python sync ```python sync
page.locator("button", has_text="Click me").click() page.get_by_test_id("product-card").filter(has_text="Product 3").click()
page.locator("button", has_text=re.compile("Click me")).click() page.get_by_test_id("product-card").filter(has_text=re.compile("Product 3")).click()
``` ```
```csharp ```csharp
await page.Locator("button", new() { HasText = "Click me" }).ClickAsync(); await page.GetByTestId("product-card").Filter(new() { HasText = "Product 3" }).ClickAsync();
await page.Locator("button", new() { HasText = new Regex("Click me") }).ClickAsync(); await page.GetByTestId("product-card").Filter(new() { HasText = new Regex("Product 3") }).ClickAsync();
``` ```
#### Filter by another locator #### Filter by another locator
@ -446,19 +612,19 @@ await page.Locator("button", new() { HasText = new Regex("Click me") }).ClickAsy
Locators support an option to only select elements that have a descendant matching another locator. Locators support an option to only select elements that have a descendant matching another locator.
```js ```js
page.locator('article', { has: page.locator('button.subscribe') }) page.getByRole('section').filter({ has: page.getByTestId('subscribe-button') })
``` ```
```java ```java
page.locator("article", new Page.LocatorOptions().setHas(page.locator("button.subscribe"))) page.getByRole("section").filter(new Locator.FilterOptions().setHas(page.getByTestId("subscribe-button")))
``` ```
```python async ```python async
page.locator("article", has=page.locator("button.subscribe")) page.get_by_role("section").filter(has=page.get_by_test_id("subscribe-button"))
``` ```
```python sync ```python sync
page.locator("article", has=page.locator("button.subscribe")) page.get_by_role("section").filter(has=page.get_by_test_id("subscribe-button"))
``` ```
```csharp ```csharp
page.Locator("article", new() { Has = page.Locator("button.subscribe") }) page.GetByRole("section"), new() { Has = page.GetByTestId("subscribe-button") })
``` ```
Note that inner locator is matched starting from the outer one, not from the document root. Note that inner locator is matched starting from the outer one, not from the document root.
@ -472,7 +638,7 @@ const rowLocator = page.locator('tr');
// ... // ...
await rowLocator await rowLocator
.filter({ hasText: 'text in column 1' }) .filter({ hasText: 'text in column 1' })
.filter({ has: page.locator('button', { hasText: 'column 2 button' }) }) .filter({ has: page.getByRole('button', { name: 'column 2 button' }) })
.screenshot(); .screenshot();
``` ```
```java ```java
@ -481,7 +647,7 @@ Locator rowLocator = page.locator("tr");
rowLocator rowLocator
.filter(new Locator.FilterOptions().setHasText("text in column 1")) .filter(new Locator.FilterOptions().setHasText("text in column 1"))
.filter(new Locator.FilterOptions().setHas( .filter(new Locator.FilterOptions().setHas(
page.locator("button", new Page.LocatorOptions().setHasText("column 2 button")) page.getByRole("button", new Page.GetByRoleOptions().setName("column 2 button"))
)) ))
.screenshot(); .screenshot();
``` ```
@ -490,7 +656,7 @@ row_locator = page.locator("tr")
# ... # ...
await row_locator await row_locator
.filter(has_text="text in column 1") .filter(has_text="text in column 1")
.filter(has=page.locator("tr", has_text="column 2 button")) .filter(has=page.get_by_role("button", name="column 2 button"))
.screenshot() .screenshot()
``` ```
```python sync ```python sync
@ -498,7 +664,7 @@ row_locator = page.locator("tr")
# ... # ...
row_locator row_locator
.filter(has_text="text in column 1") .filter(has_text="text in column 1")
.filter(has=page.locator("tr", has_text="column 2 button")) .filter(has=page.get_by_role("button", name="column 2 button"))
.screenshot() .screenshot()
``` ```
```csharp ```csharp
@ -507,7 +673,7 @@ var rowLocator = page.Locator("tr");
await rowLocator await rowLocator
.Filter(new LocatorFilterOptions { HasText = "text in column 1" }) .Filter(new LocatorFilterOptions { HasText = "text in column 1" })
.Filter(new LocatorFilterOptions { .Filter(new LocatorFilterOptions {
Has = page.Locator("tr", new PageLocatorOptions { HasText = "column 2 button" } ) Has = page.GetByRole("button", new() { Name = "column 2 button" } )
}) })
.ScreenshotAsync(); .ScreenshotAsync();
``` ```
@ -515,21 +681,21 @@ await rowLocator
### Locate elements in Shadow DOM ### Locate elements in Shadow DOM
All locators in Playwright **by default** work with elements in Shadow DOM. The exceptions are: All locators in Playwright **by default** work with elements in Shadow DOM. The exceptions are:
- Locating by XPath selector does not pierce shadow roots. - Locating by XPath does not pierce shadow roots.
- [Closed-mode shadow roots](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#parameters) are not supported. - [Closed-mode shadow roots](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#parameters) are not supported.
Consider the following example with a custom web component: Consider the following example with a custom web component:
```html ```html
<x-badge> <x-details role=button aria-expanded=true aria-controls=inner-details>
<span>Title</span> <div>Title</div>
#shadow-root #shadow-root
<span>Details</span> <div id=inner-details>Details</div>
</x-badge> </x-details>
``` ```
You can locate in the same way as if the shadow root was not present at all. You can locate in the same way as if the shadow root was not present at all.
- Click `<span>Details</span>` - Click `<div>Details</div>`
```js ```js
await page.getByText('Details').click(); await page.getByText('Details').click();
``` ```
@ -546,38 +712,38 @@ You can locate in the same way as if the shadow root was not present at all.
await page.GetByText("Details").ClickAsync(); await page.GetByText("Details").ClickAsync();
``` ```
- Click `<x-badge>` - Click `<x-details>`
```js ```js
await page.locator('x-badge', { hasText: 'Details' }).click(); await page.locator('x-details', { hasText: 'Details' }).click();
``` ```
```java ```java
page.locator("x-badge", new Page.LocatorOptions().setHasText("Details")).click(); page.locator("x-details", new Page.LocatorOptions().setHasText("Details")).click();
``` ```
```python async ```python async
await page.locator("x-badge", has_text="Details" ).click() await page.locator("x-details", has_text="Details" ).click()
``` ```
```python sync ```python sync
page.locator("x-badge", has_text="Details" ).click() page.locator("x-details", has_text="Details" ).click()
``` ```
```csharp ```csharp
await page.Locator("x-badge", new() { HasText = "Details" }).ClickAsync(); await page.Locator("x-details", new() { HasText = "Details" }).ClickAsync();
``` ```
- Ensure that `<x-badge>` contains text "Details" - Ensure that `<x-details>` contains text "Details"
```js ```js
await expect(page.locator('x-badge')).toContainText('Details'); await expect(page.locator('x-details')).toContainText('Details');
``` ```
```java ```java
assertThat(page.locator("x-badge")).containsText("Details"); assertThat(page.locator("x-details")).containsText("Details");
``` ```
```python async ```python async
await expect(page.locator("x-badge")).to_contain_text("Details") await expect(page.locator("x-details")).to_contain_text("Details")
``` ```
```python sync ```python sync
expect(page.locator("x-badge")).to_contain_text("Details") expect(page.locator("x-details")).to_contain_text("Details")
``` ```
```csharp ```csharp
await Expect(page.Locator("x-badge")).ToContainTextAsync("Details"); await Expect(page.Locator("x-details")).ToContainTextAsync("Details");
``` ```
## Lists ## Lists
@ -586,7 +752,7 @@ You can also use locators to work with the element lists.
```js ```js
// Locate elements, this locator points to a list. // Locate elements, this locator points to a list.
const rows = page.locator('table tr'); const rows = page.getByRole('listitem');
// Pattern 1: use locator methods to calculate text on the whole list. // Pattern 1: use locator methods to calculate text on the whole list.
const texts = await rows.allTextContents(); const texts = await rows.allTextContents();
@ -603,7 +769,7 @@ const texts = await rows.evaluateAll(list => list.map(element => element.textCon
```python async ```python async
# Locate elements, this locator points to a list. # Locate elements, this locator points to a list.
rows = page.locator("table tr") rows = page.get_by_role("listitem")
# Pattern 1: use locator methods to calculate text on the whole list. # Pattern 1: use locator methods to calculate text on the whole list.
texts = await rows.all_text_contents() texts = await rows.all_text_contents()
@ -620,7 +786,7 @@ texts = await rows.evaluate_all("list => list.map(element => element.textContent
```python sync ```python sync
# Locate elements, this locator points to a list. # Locate elements, this locator points to a list.
rows = page.locator("table tr") rows = page.get_by_role("listitem")
# Pattern 1: use locator methods to calculate text on the whole list. # Pattern 1: use locator methods to calculate text on the whole list.
texts = rows.all_text_contents() texts = rows.all_text_contents()
@ -637,7 +803,7 @@ texts = rows.evaluate_all("list => list.map(element => element.textContent)")
```java ```java
// Locate elements, this locator points to a list. // Locate elements, this locator points to a list.
Locator rows = page.locator("table tr"); Locator rows = page.getByRole("listitem");
// Pattern 1: use locator methods to calculate text on the whole list. // Pattern 1: use locator methods to calculate text on the whole list.
List<String> texts = rows.allTextContents(); List<String> texts = rows.allTextContents();
@ -654,7 +820,7 @@ Object texts = rows.evaluateAll("list => list.map(element => element.textContent
```csharp ```csharp
// Locate elements, this locator points to a list. // Locate elements, this locator points to a list.
var rows = page.Locator("table tr"); var rows = page.GetByRole("listitem");
// Pattern 1: use locator methods to calculate text on the whole list. // Pattern 1: use locator methods to calculate text on the whole list.
var texts = await rows.AllTextContentsAsync(); var texts = await rows.AllTextContentsAsync();
@ -673,101 +839,26 @@ var texts = await rows.EvaluateAllAsync("list => list.map(element => element.tex
If you have a list of identical elements, and the only way to distinguish between them is the order, you can choose a specific element from a list with [`method: Locator.first`], [`method: Locator.last`] or [`method: Locator.nth`]. If you have a list of identical elements, and the only way to distinguish between them is the order, you can choose a specific element from a list with [`method: Locator.first`], [`method: Locator.last`] or [`method: Locator.nth`].
However, use these methods with caution. Often times, the page might change, and locator will point to a completely different element from the one you expected. Instead, try to come up with a unique locator that will pass the [strictness criteria](#strictness).
For example, to click the third item in the list of products: For example, to click the third item in the list of products:
```js ```js
await page.locator('data-test-id=product-card').nth(3).click(); await page.getByTestId('product-card').nth(3).click();
``` ```
```java ```java
page.locator("data-test-id=product-card").nth(3).click(); page.getByTestId("product-card").nth(3).click();
``` ```
```python async ```python async
await page.locator("data-test-id=product-card").nth(3).click() await page.get_by_test_id("product-card").nth(3).click()
``` ```
```python sync ```python sync
page.locator("data-test-id=product-card").nth(3).click() page.get_by_test_id("product-card").nth(3).click()
``` ```
```csharp ```csharp
await page.Locator("data-test-id=product-card").Nth(3).ClickAsync(); await page.GetByTestId("product-card").Nth(3).ClickAsync();
``` ```
## Locator vs ElementHandle However, use these methods with caution. Often times, the page might change, and locator will point to a completely different element from the one you expected. Instead, try to come up with a unique locator that will pass the [strictness criteria](#strictness).
:::caution
We only recommend using [ElementHandle] in the rare cases when you need to perform extensive DOM traversal
on a static page. For all user actions and assertions use locator instead.
:::
The difference between the [Locator] and [ElementHandle] is that the latter points to a particular element, while Locator captures the logic of how to retrieve that element.
In the example below, handle points to a particular DOM element on page. If that element changes text or is used by React to render an entirely different component, handle is still pointing to that very stale DOM element. This can lead to unexpected behaviors.
```js
const handle = await page.$('text=Submit');
// ...
await handle.hover();
await handle.click();
```
```java
ElementHandle handle = page.querySelector("text=Submit");
handle.hover();
handle.click();
```
```python async
handle = await page.query_selector("text=Submit")
await handle.hover()
await handle.click()
```
```python sync
handle = page.query_selector("text=Submit")
handle.hover()
handle.click()
```
```csharp
var handle = await page.QuerySelectorAsync("text=Submit");
await handle.HoverAsync();
await handle.ClickAsync();
```
With the locator, every time the locator is used, up-to-date DOM element is located in the page using the selector. So in the snippet below, underlying DOM element is going to be located twice.
```js
const locator = page.getByText('Submit');
// ...
await locator.hover();
await locator.click();
```
```java
Locator locator = page.getByText("Submit");
locator.hover();
locator.click();
```
```python async
locator = page.get_by_text("Submit")
await locator.hover()
await locator.click()
```
```python sync
locator = page.get_by_text("Submit")
locator.hover()
locator.click()
```
```csharp
var locator = page.GetByText("Submit");
await locator.HoverAsync();
await locator.ClickAsync();
```

View file

@ -4,6 +4,55 @@ title: "Release notes"
toc_max_heading_level: 2 toc_max_heading_level: 2
--- ---
## Version 1.27
### Locators
With these new APIs writing locators is a joy:
- [`method: Page.getByText`] to locate by text content.
- [`method: Page.getByRole`] to locate by [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles), [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
- [`method: Page.getByLabel`] to locate a form control by associated label's text.
- [`method: Page.getByTestId`] to locate an element based on its `data-testid` attribute (other attribute can be configured).
- [`method: Page.getByPlaceholder`] to locate an input by placeholder.
- [`method: Page.getByAltText`] to locate an element, usually image, by its text alternative.
- [`method: Page.getByTitle`] to locate an element by its title.
```csharp
await page.GetByLabel("User Name").FillAsync("John");
await page.GetByLabel("Password").FillAsync("secret-password");
await page.GetByRole("button", new() { NameString = "Sign in" }).ClickAsync();
await Expect(page.GetByText("Welcome, John!")).ToBeVisibleAsync();
```
All the same methods are also available on [Locator], [FrameLocator] and [Frame] classes.
### Other highlights
- As announced in v1.25, Ubuntu 18 will not be supported as of Dec 2022. In addition to that, there will be no WebKit updates on Ubuntu 18 starting from the next Playwright release.
### Behavior Changes
- [`method: LocatorAssertions.toHaveAttribute`] with an empty value does not match missing attribute anymore. For example, the following snippet will succeed when `button` **does not** have a `disabled` attribute.
```js
await Expect(page.GetByRole("button")).ToHaveAttribute("disabled", "");
```
### Browser Versions
* Chromium 107.0.5304.18
* Mozilla Firefox 105.0.1
* WebKit 16.0
This version was also tested against the following stable channels:
* Google Chrome 106
* Microsoft Edge 106
## Version 1.26 ## Version 1.26
### Assertions ### Assertions
@ -30,7 +79,7 @@ await Page.GotoAsync("https://playwright.dev", new() { WaitUntil = WaitUntilStat
``` ```
Prior to 1.26, this would wait for all iframes to fire the `DOMContentLoaded` Prior to 1.26, this would wait for all iframes to fire the `DOMContentLoaded`
event. event.
To align with web specification, the `WaitUntilState.DOMContentLoaded` value only waits for To align with web specification, the `WaitUntilState.DOMContentLoaded` value only waits for
the target frame to fire the `'DOMContentLoaded'` event. Use `WaitUntil: WaitUntilState.Load` to wait for all iframes. the target frame to fire the `'DOMContentLoaded'` event. Use `WaitUntil: WaitUntilState.Load` to wait for all iframes.
@ -57,7 +106,7 @@ The following does now work:
```xml ```xml
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RunSettings> <RunSettings>
<!-- Playwright --> <!-- Playwright -->
<Playwright> <Playwright>
<BrowserName>chromium</BrowserName> <BrowserName>chromium</BrowserName>
<ExpectTimeout>5000</ExpectTimeout> <ExpectTimeout>5000</ExpectTimeout>
@ -106,7 +155,7 @@ if you encounter any issues!
Linux support looks like this: Linux support looks like this:
| | Ubuntu 18.04 | Ubuntu 20.04 | Ubuntu 22.04 | Debian 11 | | Ubuntu 18.04 | Ubuntu 20.04 | Ubuntu 22.04 | Debian 11
| :--- | :---: | :---: | :---: | :---: | | :--- | :---: | :---: | :---: | :---: |
| Chromium | ✅ | ✅ | ✅ | ✅ | | Chromium | ✅ | ✅ | ✅ | ✅ |
| WebKit | ✅ | ✅ | ✅ | ✅ | | WebKit | ✅ | ✅ | ✅ | ✅ |
| Firefox | ✅ | ✅ | ✅ | ✅ | | Firefox | ✅ | ✅ | ✅ | ✅ |
@ -518,7 +567,7 @@ Its now possible to emulate the `forced-colors` CSS media feature by passing it
- [Tracing.StopChunkAsync()](https://playwright.dev/dotnet/docs/next/api/class-tracing#tracing-stop-chunk) - Stops a new trace chunk. - [Tracing.StopChunkAsync()](https://playwright.dev/dotnet/docs/next/api/class-tracing#tracing-stop-chunk) - Stops a new trace chunk.
### Important ⚠ ### Important ⚠
* ⬆ .NET Core Apps 2.1 are **no longer** supported for our CLI tooling. As of August 31st, 2021, .NET Core 2.1 is no [longer supported](https://devblogs.microsoft.com/dotnet/net-core-2-1-will-reach-end-of-support-on-august-21-2021/) and will not receive any security updates. We've decided to move the CLI forward and require .NET Core 3.1 as a minimum. * ⬆ .NET Core Apps 2.1 are **no longer** supported for our CLI tooling. As of August 31st, 2021, .NET Core 2.1 is no [longer supported](https://devblogs.microsoft.com/dotnet/net-core-2-1-will-reach-end-of-support-on-august-21-2021/) and will not receive any security updates. We've decided to move the CLI forward and require .NET Core 3.1 as a minimum.
### Browser Versions ### Browser Versions

View file

@ -4,6 +4,55 @@ title: "Release notes"
toc_max_heading_level: 2 toc_max_heading_level: 2
--- ---
## Version 1.27
### Locators
With these new APIs writing locators is a joy:
- [`method: Page.getByText`] to locate by text content.
- [`method: Page.getByRole`] to locate by [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles), [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
- [`method: Page.getByLabel`] to locate a form control by associated label's text.
- [`method: Page.getByTestId`] to locate an element based on its `data-testid` attribute (other attribute can be configured).
- [`method: Page.getByPlaceholder`] to locate an input by placeholder.
- [`method: Page.getByAltText`] to locate an element, usually image, by its text alternative.
- [`method: Page.getByTitle`] to locate an element by its title.
```java
page.getByLabel("User Name").fill("John");
page.getByLabel("Password").fill("secret-password");
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")).click();
assertThat(page.getByText("Welcome, John!")).isVisible();
```
All the same methods are also available on [Locator], [FrameLocator] and [Frame] classes.
### Other highlights
- As announced in v1.25, Ubuntu 18 will not be supported as of Dec 2022. In addition to that, there will be no WebKit updates on Ubuntu 18 starting from the next Playwright release.
### Behavior Changes
- [`method: LocatorAssertions.toHaveAttribute`] with an empty value does not match missing attribute anymore. For example, the following snippet will succeed when `button` **does not** have a `disabled` attribute.
```js
assertThat(page.getByRole(AriaRole.BUTTON)).hasAttribute("disabled", "");
```
### Browser Versions
* Chromium 107.0.5304.18
* Mozilla Firefox 105.0.1
* WebKit 16.0
This version was also tested against the following stable channels:
* Google Chrome 106
* Microsoft Edge 106
## Version 1.26 ## Version 1.26
### Assertions ### Assertions
@ -28,7 +77,7 @@ page.navigate("https://playwright.dev", new Page.NavigateOptions().setWaitUntil(
``` ```
Prior to 1.26, this would wait for all iframes to fire the `DOMContentLoaded` Prior to 1.26, this would wait for all iframes to fire the `DOMContentLoaded`
event. event.
To align with web specification, the `WaitUntilState.DOMCONTENTLOADED` value only waits for To align with web specification, the `WaitUntilState.DOMCONTENTLOADED` value only waits for
the target frame to fire the `'DOMContentLoaded'` event. Use `setWaitUntil(WaitUntilState.LOAD)` to wait for all iframes. the target frame to fire the `'DOMContentLoaded'` event. Use `setWaitUntil(WaitUntilState.LOAD)` to wait for all iframes.
@ -80,7 +129,7 @@ if you encounter any issues!
Linux support looks like this: Linux support looks like this:
| | Ubuntu 18.04 | Ubuntu 20.04 | Ubuntu 22.04 | Debian 11 | | Ubuntu 18.04 | Ubuntu 20.04 | Ubuntu 22.04 | Debian 11
| :--- | :---: | :---: | :---: | :---: | | :--- | :---: | :---: | :---: | :---: |
| Chromium | ✅ | ✅ | ✅ | ✅ | | Chromium | ✅ | ✅ | ✅ | ✅ |
| WebKit | ✅ | ✅ | ✅ | ✅ | | WebKit | ✅ | ✅ | ✅ | ✅ |
| Firefox | ✅ | ✅ | ✅ | ✅ | | Firefox | ✅ | ✅ | ✅ | ✅ |

View file

@ -12,6 +12,7 @@ With these new APIs writing locators is a joy:
- [`method: Page.getByText`] to locate by text content. - [`method: Page.getByText`] to locate by text content.
- [`method: Page.getByRole`] to locate by [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles), [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). - [`method: Page.getByRole`] to locate by [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles), [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
- [`method: Page.getByLabel`] to locate a form control by associated label's text. - [`method: Page.getByLabel`] to locate a form control by associated label's text.
- [`method: Page.getByTestId`] to locate an element based on its `data-testid` attribute (other attribute can be configured).
- [`method: Page.getByPlaceholder`] to locate an input by placeholder. - [`method: Page.getByPlaceholder`] to locate an input by placeholder.
- [`method: Page.getByAltText`] to locate an element, usually image, by its text alternative. - [`method: Page.getByAltText`] to locate an element, usually image, by its text alternative.
- [`method: Page.getByTitle`] to locate an element by its title. - [`method: Page.getByTitle`] to locate an element by its title.
@ -143,7 +144,7 @@ This version was also tested against the following stable channels:
### Announcements ### Announcements
* 🎁 We now ship Ubuntu 22.04 Jammy Jellyfish docker image: `mcr.microsoft.com/playwright:v1.27.0-jammy`. * 🎁 We now ship Ubuntu 22.04 Jammy Jellyfish docker image: `mcr.microsoft.com/playwright:v1.27.1-jammy`.
* 🪦 This is the last release with macOS 10.15 support (deprecated as of 1.21). * 🪦 This is the last release with macOS 10.15 support (deprecated as of 1.21).
* 🪦 This is the last release with Node.js 12 support, we recommend upgrading to Node.js LTS (16). * 🪦 This is the last release with Node.js 12 support, we recommend upgrading to Node.js LTS (16).
* ⚠️ Ubuntu 18 is now deprecated and will not be supported as of Dec 2022. * ⚠️ Ubuntu 18 is now deprecated and will not be supported as of Dec 2022.
@ -393,7 +394,7 @@ Read more about [component testing with Playwright](./test-components).
} }
}); });
``` ```
* Playwright now runs on Ubuntu 22 amd64 and Ubuntu 22 arm64. We also publish new docker image `mcr.microsoft.com/playwright:v1.27.0-jammy`. * Playwright now runs on Ubuntu 22 amd64 and Ubuntu 22 arm64. We also publish new docker image `mcr.microsoft.com/playwright:v1.27.1-jammy`.
### ⚠️ Breaking Changes ⚠️ ### ⚠️ Breaking Changes ⚠️

View file

@ -4,6 +4,55 @@ title: "Release notes"
toc_max_heading_level: 2 toc_max_heading_level: 2
--- ---
## Version 1.27
### Locators
With these new APIs writing locators is a joy:
- [`method: Page.getByText`] to locate by text content.
- [`method: Page.getByRole`] to locate by [ARIA role](https://www.w3.org/TR/wai-aria-1.2/#roles), [ARIA attributes](https://www.w3.org/TR/wai-aria-1.2/#aria-attributes) and [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
- [`method: Page.getByLabel`] to locate a form control by associated label's text.
- [`method: Page.getByTestId`] to locate an element based on its `data-testid` attribute (other attribute can be configured).
- [`method: Page.getByPlaceholder`] to locate an input by placeholder.
- [`method: Page.getByAltText`] to locate an element, usually image, by its text alternative.
- [`method: Page.getByTitle`] to locate an element by its title.
```python
page.get_by_label("User Name").fill("John")
page.get_by_label("Password").fill("secret-password")
page.get_by_role("button", name="Sign in").click()
expect(page.get_by_text("Welcome, John!")).to_be_visible()
```
All the same methods are also available on [Locator], [FrameLocator] and [Frame] classes.
### Other highlights
- As announced in v1.25, Ubuntu 18 will not be supported as of Dec 2022. In addition to that, there will be no WebKit updates on Ubuntu 18 starting from the next Playwright release.
### Behavior Changes
- [`method: LocatorAssertions.toHaveAttribute`] with an empty value does not match missing attribute anymore. For example, the following snippet will succeed when `button` **does not** have a `disabled` attribute.
```js
expect(page.get_by_role("button")).to_have_attribute("disabled", "")
```
### Browser Versions
* Chromium 107.0.5304.18
* Mozilla Firefox 105.0.1
* WebKit 16.0
This version was also tested against the following stable channels:
* Google Chrome 106
* Microsoft Edge 106
## Version 1.26 ## Version 1.26
### Assertions ### Assertions
@ -28,7 +77,7 @@ page.goto("https://playwright.dev", wait_until="domcontentloaded")
``` ```
Prior to 1.26, this would wait for all iframes to fire the `DOMContentLoaded` Prior to 1.26, this would wait for all iframes to fire the `DOMContentLoaded`
event. event.
To align with web specification, the `'domcontentloaded'` value only waits for To align with web specification, the `'domcontentloaded'` value only waits for
the target frame to fire the `'DOMContentLoaded'` event. Use `wait_until="load"` to wait for all iframes. the target frame to fire the `'DOMContentLoaded'` event. Use `wait_until="load"` to wait for all iframes.
@ -48,7 +97,7 @@ This version was also tested against the following stable channels:
### Announcements ### Announcements
* 🎁 We now ship Ubuntu 22.04 Jammy Jellyfish docker image: `mcr.microsoft.com/playwright/python:v1.27.0-jammy`. * 🎁 We now ship Ubuntu 22.04 Jammy Jellyfish docker image: `mcr.microsoft.com/playwright/python:v1.27.1-jammy`.
* 🪦 This is the last release with macOS 10.15 support (deprecated as of 1.21). * 🪦 This is the last release with macOS 10.15 support (deprecated as of 1.21).
* ⚠️ Ubuntu 18 is now deprecated and will not be supported as of Dec 2022. * ⚠️ Ubuntu 18 is now deprecated and will not be supported as of Dec 2022.
@ -77,7 +126,7 @@ if you encounter any issues!
Linux support looks like this: Linux support looks like this:
| | Ubuntu 18.04 | Ubuntu 20.04 | Ubuntu 22.04 | Debian 11 | | Ubuntu 18.04 | Ubuntu 20.04 | Ubuntu 22.04 | Debian 11
| :--- | :---: | :---: | :---: | :---: | | :--- | :---: | :---: | :---: | :---: |
| Chromium | ✅ | ✅ | ✅ | ✅ | | Chromium | ✅ | ✅ | ✅ | ✅ |
| WebKit | ✅ | ✅ | ✅ | ✅ | | WebKit | ✅ | ✅ | ✅ | ✅ |
| Firefox | ✅ | ✅ | ✅ | ✅ | | Firefox | ✅ | ✅ | ✅ | ✅ |

View file

@ -82,7 +82,7 @@ Since Playwright runs in Node.js, you can debug it with your debugger of choice
npx playwright test example.spec.ts:42 --debug npx playwright test example.spec.ts:42 --debug
``` ```
<img width="1188" alt="Debugging Tests" src="https://user-images.githubusercontent.com/13063165/181847661-7ec5fb6c-7c21-4db0-9931-a593b21bafc2.png" /> <img width="1350" alt="Debugging Tests with the Playwright inspector" src="https://user-images.githubusercontent.com/13063165/197800771-50cb2f39-2345-4153-b4ed-de9fe63ba29b.png" />
Check out our [debugging guide](./debug.md) to learn more about the [Playwright Inspector](./debug.md#playwright-inspector) as well as debugging with [Browser Developer tools](./debug.md#browser-developer-tools). Check out our [debugging guide](./debug.md) to learn more about the [Playwright Inspector](./debug.md#playwright-inspector) as well as debugging with [Browser Developer tools](./debug.md#browser-developer-tools).

View file

@ -236,7 +236,7 @@ await page.Locator("text=Log in").ClickAsync();
Text selector has a few variations: Text selector has a few variations:
- `text=Log in` - default matching is case-insensitive and searches for a substring. For example, `text=Log` matches `<button>Log in</button>`. - `text=Log in` - default matching is case-insensitive, trims whitespace and searches for a substring. For example, `text=Log` matches `<button>Log in</button>`.
```js ```js
await page.locator('text=Log in').click(); await page.locator('text=Log in').click();
@ -254,7 +254,7 @@ Text selector has a few variations:
await page.Locator("text=Log in").ClickAsync(); await page.Locator("text=Log in").ClickAsync();
``` ```
- `text="Log in"` - text body can be escaped with single or double quotes to search for a text node with exact content. For example, `text="Log"` does not match `<button>Log in</button>` because `<button>` contains a single text node `"Log in"` that is not equal to `"Log"`. However, `text="Log"` matches `<button>Log<span>in</span></button>`, because `<button>` contains a text node `"Log"`. This exact mode implies case-sensitive matching, so `text="Download"` will not match `<button>download</button>`. - `text="Log in"` - text body can be escaped with single or double quotes to search for a text node with exact content after trimming whitespace. For example, `text="Log"` does not match `<button>Log in</button>` because `<button>` contains a single text node `"Log in"` that is not equal to `"Log"`. However, `text="Log"` matches `<button> Log <span>in</span></button>`, because `<button>` contains a text node `" Log "`. This exact mode implies case-sensitive matching, so `text="Download"` will not match `<button>download</button>`.
Quoted body follows the usual escaping rules, e.g. use `\"` to escape double quote in a double-quoted string: `text="foo\"bar"`. Quoted body follows the usual escaping rules, e.g. use `\"` to escape double quote in a double-quoted string: `text="foo\"bar"`.
@ -310,7 +310,7 @@ Text selector has a few variations:
await page.Locator("text=/Log\\s*in/i").ClickAsync(); await page.Locator("text=/Log\\s*in/i").ClickAsync();
``` ```
- `article:has-text("Playwright")` - the `:has-text()` pseudo-class can be used inside a [css] selector. It matches any element containing specified text somewhere inside, possibly in a child or a descendant element. Matching is case-insensitive and searches for a substring. For example, `article:has-text("Playwright")` matches `<article><div>Playwright</div></article>`. - `article:has-text("Playwright")` - the `:has-text()` pseudo-class can be used inside a [css] selector. It matches any element containing specified text somewhere inside, possibly in a child or a descendant element. Matching is case-insensitive, trims whitestapce and searches for a substring. For example, `article:has-text("Playwright")` matches `<article><div>Playwright</div></article>`.
Note that `:has-text()` should be used together with other `css` specifiers, otherwise it will match all the elements containing specified text, including the `<body>`. Note that `:has-text()` should be used together with other `css` specifiers, otherwise it will match all the elements containing specified text, including the `<body>`.
```js ```js

View file

@ -56,7 +56,7 @@ The snapshot name `example-test-1-chromium-darwin.png` consists of a few parts:
If you are not on the same operating system as your CI system, you can use Docker to generate/update the screenshots: If you are not on the same operating system as your CI system, you can use Docker to generate/update the screenshots:
```bash ```bash
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.27.0-focal /bin/bash docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.27.1-focal /bin/bash
npm install npm install
npx playwright test --update-snapshots npx playwright test --update-snapshots
``` ```

View file

@ -3,15 +3,133 @@ id: trace-viewer
title: "Trace Viewer" title: "Trace Viewer"
--- ---
Playwright Trace Viewer is a GUI tool that helps exploring recorded Playwright traces after the script ran. Open traces [locally](#viewing-the-trace) or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev).
<img width="1212" alt="Playwright Trace Viewer" src="https://user-images.githubusercontent.com/883973/120585896-6a1bca80-c3e7-11eb-951a-bd84002480f5.png"></img> Playwright Trace Viewer is a GUI tool that helps you explore recorded Playwright traces after the script has ran. You can open traces [locally](#viewing-the-trace) or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev).
## Recording a trace <video width="100%" height="100%" controls muted>
<source src="https://user-images.githubusercontent.com/13063165/194582806-a26efd72-746e-40cc-8955-fa65aa3274c3.mp4
" type="video/mp4" />
Your browser does not support the video tag.
</video>
## Viewing the trace
You can open the saved trace using Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev).
```bash js
npx playwright show-trace trace.zip
```
```bash java
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="show-trace trace.zip"
```
```bash python
playwright show-trace trace.zip
```
```bash csharp
pwsh bin/Debug/netX/playwright.ps1 show-trace trace.zip
```
## Actions
Once trace is opened, you will see the list of actions Playwright performed on the left hand side:
<img width="300" alt="Trace Viewer Actions Tab" src="https://user-images.githubusercontent.com/13063165/189152329-23e965de-581e-4a20-aed7-12cbf0583c92.png" />
<br/><br/>
**Selecting each action reveals:**
- action snapshots,
- action log,
- source code location,
- network log for this action
In the properties pane you will also see rendered DOM snapshots associated with each action.
## Metadata
See metadata such as the time the action was performed, what browser engine was used, what the viewport was and if it was mobile and how many pages, actions and events were recorded.
<img width="296" alt="Trace Viewer Metadata Tab" src="https://user-images.githubusercontent.com/13063165/189155450-3865a993-cb45-439c-a02f-1ddfe60a1719.png" />
## Screenshots
When tracing with the [`option: screenshots`] option turned on, each trace records a screencast and renders it as a film strip:
<img width="1078" alt="Playwright Trace viewer > Film strip" src="https://user-images.githubusercontent.com/13063165/189174647-3e647d3d-6500-4be2-a237-9191f418eb12.png" />
<br/><br/>
You can hover over the film strip to see a magnified image of for each action and state which helps you easily find the action you want to inspect.
<img width="819" alt="Playwright Trace viewer magnify" src="https://user-images.githubusercontent.com/13063165/189174658-ba218339-2abc-4336-812e-526dbc4d2907.png" />
## Snapshots
When tracing with the [`option: snapshots`] option turned on, Playwright captures a set of complete DOM snapshots for each action. Depending on the type of the action, it will capture:
| Type | Description |
|------|-------------|
|Before|A snapshot at the time action is called.|
|Action|A snapshot at the moment of the performed input. This type of snapshot is especially useful when exploring where exactly Playwright clicked.|
|After|A snapshot after the action.|
<br/>
Here is what the typical Action snapshot looks like:
<img width="634" alt="Playwright Trace Viewer > Snapshots" src="https://user-images.githubusercontent.com/13063165/189153245-0bdcad4d-16a3-4a71-90d8-71a8038c0720.png" />
Notice how it highlights both, the DOM Node as well as the exact click position.
## Call
See what action was called, the time and duration as well as parameters, return value and log.
<img width="321" alt="Trace Viewer Call Tab" src="https://user-images.githubusercontent.com/13063165/189155306-3c9275bc-d4cd-4e91-8b63-225832a66f51.png" />
## Console
See the console output for the action where you can see console logs or errors.
<img width="299" alt="Trace Viewer Console Tab" src="https://user-images.githubusercontent.com/13063165/189173154-41d438dd-9334-4664-8c77-ee85f5040061.png" />
## Network
See any network requests that were made during the action.
<img width="321" alt="Trace Viewer Network Tab" src="https://user-images.githubusercontent.com/13063165/189155367-e19f1c89-4e62-4258-970d-6a740e891711.png" />
## Source
See the source code for your entire test.
<img width="476" alt="Trace Viewer Source Tab" src="https://user-images.githubusercontent.com/13063165/189155239-c0f6045c-ab67-404a-8140-e98f78c58ae1.png" />
## Recording a trace locally
* langs: js * langs: js
Set the `trace: 'on-first-retry'` option in the test configuration file. This will produce `trace.zip` file for each test that was retried. To record a trace during development mode set the `--trace` flag to `on` when running your tests.
```bash
npx playwright test --trace on
```
You can then open the HTML report and click on the trace icon to open the trace.
```bash
npx playwright show-report
```
## Recording a trace on CI
Traces should be run on continuous integration on the first retry of a failed test
by setting the `trace: 'on-first-retry'` option in the test configuration file. This will produce a `trace.zip` file for each test that was retried.
```js tab=js-js ```js tab=js-js
// @ts-check // @ts-check
@ -61,7 +179,7 @@ Available options to record a trace:
You can also use `trace: 'retain-on-failure'` if you do not enable retries but still want traces for failed tests. You can also use `trace: 'retain-on-failure'` if you do not enable retries but still want traces for failed tests.
If you are not using Playwright Test, use the [`property: BrowserContext.tracing`] API instead. If you are not using Playwright as a Test Runner, use the [`property: BrowserContext.tracing`] API instead.
## Recording a trace ## Recording a trace
* langs: java, csharp, python * langs: java, csharp, python
@ -156,56 +274,16 @@ playwright show-trace trace.zip
pwsh bin/Debug/netX/playwright.ps1 show-trace trace.zip pwsh bin/Debug/netX/playwright.ps1 show-trace trace.zip
``` ```
## Actions ## Using [trace.playwright.dev](https://trace.playwright.dev)
Once trace is opened, you will see the list of actions Playwright performed on the left hand side: [trace.playwright.dev](https://trace.playwright.dev) is a statically hosted variant of the Trace Viewer. You can upload trace files using drag and drop.
<img width="301" alt="Actions" src="https://user-images.githubusercontent.com/883973/120588303-d39dd800-c3eb-11eb-9e8b-bfea8b775354.png"></img>
Selecting each action reveals:
- action snapshots,
- action log,
- source code location,
- network log for this action
in the properties pane. You will also see rendered DOM snapshots associated with each action.
## Screenshots
When tracing with the [`option: screenshots`] option turned on, each trace records screencast and renders it as a film strip:
<img width="353" alt="Film strip" src="https://user-images.githubusercontent.com/883973/120588069-5d997100-c3eb-11eb-97a3-acbd5e0eb358.png"></img>
You can hover over the film strip to see a magnified image:
<img width="617" alt="Magnify" src="https://user-images.githubusercontent.com/883973/120588147-8f123c80-c3eb-11eb-864b-19d800619234.png"></img>
That helps locating the action of interest very quickly.
## Snapshots
When tracing with the [`option: snapshots`] option turned on, Playwright captures a set of complete DOM snapshots for each action. Depending on the type of the action, it will capture:
| Type | Description |
|------|-------------|
|Before|A snapshot at the time action is called.|
|Action|A snapshot at the moment of the performed input. This type of snapshot is especially useful when exploring where exactly Playwright clicked.|
|After|A snapshot after the action.|
<br/>
Here is what the typical Action snapshot looks like:
<img width="682" alt="Snapshots" src="https://user-images.githubusercontent.com/883973/120588728-879f6300-c3ec-11eb-85d6-e67b0e92e4e3.png">
</img>
Notice how it highlights both, the DOM Node as well as the exact click position.
## Viewing remote Traces <img width="1119" alt="Drop Playwright Trace to load" src="https://user-images.githubusercontent.com/13063165/194577918-b4d45726-2692-4093-8a28-9e73552617ef.png" />
You can open remote traces using it's URL. ## Viewing remote traces
They could be generated in a CI run and makes it easy to view the remote trace without having to manually download the file.
You can open remote traces using it's URL. They could be generated on a CI run which makes it easy to view the remote trace without having to manually download the file.
```bash js ```bash js
npx playwright show-trace https://example.com/trace.zip npx playwright show-trace https://example.com/trace.zip
@ -223,15 +301,6 @@ playwright show-trace https://example.com/trace.zip
pwsh bin/Debug/netX/playwright.ps1 show-trace https://example.com/trace.zip pwsh bin/Debug/netX/playwright.ps1 show-trace https://example.com/trace.zip
``` ```
## Using [trace.playwright.dev](https://trace.playwright.dev)
[trace.playwright.dev](https://trace.playwright.dev) is a statically hosted variant of the Trace Viewer.
### Viewing local traces
When navigating to [trace.playwright.dev](https://trace.playwright.dev), you can upload trace files using drag and drop.
### Remote traces
You can also pass the URL of your uploaded trace (e.g. inside your CI) from some accessible storage as a parameter. CORS (Cross-Origin Resource Sharing) rules might apply. You can also pass the URL of your uploaded trace (e.g. inside your CI) from some accessible storage as a parameter. CORS (Cross-Origin Resource Sharing) rules might apply.

View file

@ -5,7 +5,7 @@ title: "Writing Tests"
Playwright assertions are created specifically for the dynamic web. Checks are automatically retried until the necessary conditions are met. Playwright comes with [auto-wait](./actionability.md) built in meaning it waits for elements to be actionable prior to performing actions. Playwright provides the [Expect](./test-assertions) function to write assertions. Playwright assertions are created specifically for the dynamic web. Checks are automatically retried until the necessary conditions are met. Playwright comes with [auto-wait](./actionability.md) built in meaning it waits for elements to be actionable prior to performing actions. Playwright provides the [Expect](./test-assertions) function to write assertions.
Take a look at the example test below to see how to write a test using web first assertions, locators and selectors. Take a look at the example test below to see how to write a test using using [locators](/locators.md) and web first assertions.
<Tabs <Tabs
groupId="test-runners" groupId="test-runners"
@ -36,7 +36,7 @@ public class Tests : PageTest
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright")); await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
// create a locator // create a locator
var getStarted = Page.Locator("text=Get Started"); var getStarted = Page.GetByRole(AriaRole.Link, new() { NameString = "Get started" });
// Expect an attribute "to be strictly equal" to the value. // Expect an attribute "to be strictly equal" to the value.
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro"); await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro");
@ -71,7 +71,7 @@ public class UnitTest1 : PageTest
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright")); await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
// create a locator // create a locator
var getStarted = Page.Locator("text=Get Started"); var getStarted = Page.GetByRole(AriaRole.Link, new() { NameString = "Get started" });
// Expect an attribute "to be strictly equal" to the value. // Expect an attribute "to be strictly equal" to the value.
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro"); await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro");
@ -99,22 +99,15 @@ await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
### Locators ### Locators
[Locators](./locators.md) are the central piece of Playwright's auto-waiting and retry-ability. Locators represent a way to find element(s) on the page at any moment and are used to perform actions on elements such as `.ClickAsync` `.FillAsync` etc. Custom locators can be created with the [`method: Page.locator`] method. [Locators](./locators.md) are the central piece of Playwright's auto-waiting and retry-ability. Locators represent a way to find element(s) on the page at any moment and are used to perform actions on elements such as `.ClickAsync` `.FillAsync` etc.
```csharp ```csharp
var getStarted = Page.Locator("text=Get Started"); var getStarted = Page.GetByRole(AriaRole.Link, new() { NameString = "Get started" });
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/installation"); await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/installation");
await getStarted.ClickAsync(); await getStarted.ClickAsync();
``` ```
[Selectors](./selectors.md) are strings that are used to create Locators. Playwright supports many different selectors like [Text](./selectors.md#text-selector), [CSS](./selectors.md#css-selector), [XPath](./selectors.md#xpath-selectors) and many more. Learn more about available selectors and how to pick one in this [in-depth guide](./selectors.md).
```csharp
await Expect(Page.Locator("text=Installation")).ToBeVisibleAsync();
```
### Test Isolation ### Test Isolation
The Playwright NUnit and MSTest test framework base classes will isolate each test from each other by providing a separate `Page` instance. Pages are isolated between tests due to the Browser Context, which is equivalent to a brand new browser profile, where every test gets a fresh environment, even when multiple tests run in a single Browser. The Playwright NUnit and MSTest test framework base classes will isolate each test from each other by providing a separate `Page` instance. Pages are isolated between tests due to the Browser Context, which is equivalent to a brand new browser profile, where every test gets a fresh environment, even when multiple tests run in a single Browser.

View file

@ -15,7 +15,7 @@ Playwright assertions are created specifically for the dynamic web. Checks are a
## The Example Test ## The Example Test
Take a look at the example test included when installing Playwright to see how to write a test using [web first assertions](/test-assertions.md), [locators](/locators.md) and [selectors](/selectors.md). Take a look at the example test included when installing Playwright to see how to write a test using [locators](/locators.md) and [web first assertions](/test-assertions.md).
```js tab=js-js ```js tab=js-js
// @ts-check // @ts-check
@ -28,7 +28,7 @@ test('homepage has Playwright in title and get started link linking to the intro
await expect(page).toHaveTitle(/Playwright/); await expect(page).toHaveTitle(/Playwright/);
// create a locator // create a locator
const getStarted = page.getByText('Get Started'); const getStarted = page.getByRole('link', { name: 'Get started' });
// Expect an attribute "to be strictly equal" to the value. // Expect an attribute "to be strictly equal" to the value.
await expect(getStarted).toHaveAttribute('href', '/docs/intro'); await expect(getStarted).toHaveAttribute('href', '/docs/intro');
@ -51,7 +51,7 @@ test('homepage has Playwright in title and get started link linking to the intro
await expect(page).toHaveTitle(/Playwright/); await expect(page).toHaveTitle(/Playwright/);
// create a locator // create a locator
const getStarted = page.getByText('Get Started'); const getStarted = page.getByRole('link', { name: 'Get started' });
// Expect an attribute "to be strictly equal" to the value. // Expect an attribute "to be strictly equal" to the value.
await expect(getStarted).toHaveAttribute('href', '/docs/intro'); await expect(getStarted).toHaveAttribute('href', '/docs/intro');
@ -79,23 +79,15 @@ await expect(page).toHaveTitle(/Playwright/);
### Locators ### Locators
[Locators](./locators.md) are the central piece of Playwright's auto-waiting and retry-ability. Locators represent a way to find element(s) on the page at any moment and are used to perform actions on elements such as `.click` `.fill` etc. Custom locators can be created with the [`method: Page.locator`] method. [Locators](./locators.md) are the central piece of Playwright's auto-waiting and retry-ability. Locators represent a way to find element(s) on the page at any moment and are used to perform actions on elements such as `.click` `.fill` etc.
```js ```js
const getStarted = page.getByText('Get Started'); const getStarted = page.getByRole('link', { name: 'Get started' });
await expect(getStarted).toHaveAttribute('href', '/docs/installation'); await expect(getStarted).toHaveAttribute('href', '/docs/installation');
await getStarted.click(); await getStarted.click();
``` ```
[Selectors](./selectors.md) are strings that are used to create Locators. Playwright supports many different selectors like [Text](./selectors.md#text-selector), [CSS](./selectors.md#css-selector), [XPath](./selectors.md#xpath-selectors) and many more. Learn more about available selectors and how to pick one in this [in-depth guide](./selectors.md).
```js
await expect(page.getByText('Installation')).toBeVisible();
```
### Test Isolation ### Test Isolation
Playwright Test is based on the concept of [test fixtures](./test-fixtures.md) such as the [built in page fixture](./test-fixtures#built-in-fixtures), which is passed into your test. Pages are isolated between tests due to the Browser Context, which is equivalent to a brand new browser profile, where every test gets a fresh environment, even when multiple tests run in a single Browser. Playwright Test is based on the concept of [test fixtures](./test-fixtures.md) such as the [built in page fixture](./test-fixtures#built-in-fixtures), which is passed into your test. Pages are isolated between tests due to the Browser Context, which is equivalent to a brand new browser profile, where every test gets a fresh environment, even when multiple tests run in a single Browser.

View file

@ -5,7 +5,7 @@ title: "Writing Tests"
Playwright assertions are created specifically for the dynamic web. Checks are automatically retried until the necessary conditions are met. Playwright comes with [auto-wait](./actionability.md) built in meaning it waits for elements to be actionable prior to performing actions. Playwright provides an [expect](./test-assertions.md) function to write assertions. Playwright assertions are created specifically for the dynamic web. Checks are automatically retried until the necessary conditions are met. Playwright comes with [auto-wait](./actionability.md) built in meaning it waits for elements to be actionable prior to performing actions. Playwright provides an [expect](./test-assertions.md) function to write assertions.
Take a look at the example test below to see how to write a test using web first assertions, locators and selectors. Take a look at the example test below to see how to write a test using [locators](/locators.md) and web first assertions.
```python ```python
import re import re
@ -19,7 +19,7 @@ def test_homepage_has_Playwright_in_title_and_get_started_link_linking_to_the_in
expect(page).to_have_title(re.compile("Playwright")) expect(page).to_have_title(re.compile("Playwright"))
# create a locator # create a locator
get_started = page.locator("text=Get Started") get_started = page.get_by_role("link", name="Get started");
# Expect an attribute "to be strictly equal" to the value. # Expect an attribute "to be strictly equal" to the value.
expect(get_started).to_have_attribute("href", "/docs/intro") expect(get_started).to_have_attribute("href", "/docs/intro")
@ -46,27 +46,17 @@ expect(page).to_have_title(re.compile("Playwright"))
### Locators ### Locators
[Locators](./locators.md) are the central piece of Playwright's auto-waiting and retry-ability. Locators represent a way to find element(s) on the page at any moment and are used to perform actions on elements such as `.click` `.fill` etc. Custom locators can be created with the [`method: Page.locator`] method. [Locators](./locators.md) are the central piece of Playwright's auto-waiting and retry-ability. Locators represent a way to find element(s) on the page at any moment and are used to perform actions on elements such as `.click` `.fill` etc.
```python ```python
from playwright.sync_api import expect from playwright.sync_api import expect
get_started = page.locator("text=Get Started") get_started = page.get_by_role("link", name="Get started");
expect(get_started).to_have_attribute("href", "/docs/installation") expect(get_started).to_have_attribute("href", "/docs/installation")
get_started.click() get_started.click()
``` ```
[Selectors](./selectors.md) are strings that are used to create Locators. Playwright supports many different selectors like [Text](./selectors.md#text-selector), [CSS](./selectors.md#css-selector), [XPath](./selectors.md#xpath-selectors) and many more. Learn more about available selectors and how to pick one in this [in-depth guide](./selectors.md).
```python
from playwright.sync_api import expect
expect(page.locator("text=Installation")).to_be_visible()
```
### Test Isolation ### Test Isolation
The Playwright Pytest plugin is based on the concept of test fixtures such as the [built in page fixture](./test-runners.md), which is passed into your test. Pages are isolated between tests due to the Browser Context, which is equivalent to a brand new browser profile, where every test gets a fresh environment, even when multiple tests run in a single Browser. The Playwright Pytest plugin is based on the concept of test fixtures such as the [built in page fixture](./test-runners.md), which is passed into your test. Pages are isolated between tests due to the Browser Context, which is equivalent to a brand new browser profile, where every test gets a fresh environment, even when multiple tests run in a single Browser.

66
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "playwright-internal", "name": "playwright-internal",
"version": "1.27.0-next", "version": "1.27.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "playwright-internal", "name": "playwright-internal",
"version": "1.27.0-next", "version": "1.27.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@ -6393,11 +6393,11 @@
"version": "0.0.0" "version": "0.0.0"
}, },
"packages/playwright": { "packages/playwright": {
"version": "1.27.0-next", "version": "1.27.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -6407,11 +6407,11 @@
} }
}, },
"packages/playwright-chromium": { "packages/playwright-chromium": {
"version": "1.27.0-next", "version": "1.27.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -6421,7 +6421,7 @@
} }
}, },
"packages/playwright-core": { "packages/playwright-core": {
"version": "1.27.0-next", "version": "1.27.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -6432,10 +6432,10 @@
}, },
"packages/playwright-ct-react": { "packages/playwright-ct-react": {
"name": "@playwright/experimental-ct-react", "name": "@playwright/experimental-ct-react",
"version": "1.27.0-next", "version": "1.27.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/test": "1.27.0-next", "@playwright/test": "1.27.1",
"@vitejs/plugin-react": "^2.0.1", "@vitejs/plugin-react": "^2.0.1",
"vite": "^3.0.9" "vite": "^3.0.9"
}, },
@ -6888,10 +6888,10 @@
}, },
"packages/playwright-ct-solid": { "packages/playwright-ct-solid": {
"name": "@playwright/experimental-ct-solid", "name": "@playwright/experimental-ct-solid",
"version": "1.27.0-next", "version": "1.27.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/test": "1.27.0-next", "@playwright/test": "1.27.1",
"vite": "^3.0.0", "vite": "^3.0.0",
"vite-plugin-solid": "^2.3.0" "vite-plugin-solid": "^2.3.0"
}, },
@ -7308,10 +7308,10 @@
}, },
"packages/playwright-ct-svelte": { "packages/playwright-ct-svelte": {
"name": "@playwright/experimental-ct-svelte", "name": "@playwright/experimental-ct-svelte",
"version": "1.27.0-next", "version": "1.27.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/test": "1.27.0-next", "@playwright/test": "1.27.1",
"@sveltejs/vite-plugin-svelte": "^1.0.1", "@sveltejs/vite-plugin-svelte": "^1.0.1",
"vite": "^3.0.0" "vite": "^3.0.0"
}, },
@ -7738,10 +7738,10 @@
}, },
"packages/playwright-ct-vue": { "packages/playwright-ct-vue": {
"name": "@playwright/experimental-ct-vue", "name": "@playwright/experimental-ct-vue",
"version": "1.27.0-next", "version": "1.27.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/test": "1.27.0-next", "@playwright/test": "1.27.1",
"@vitejs/plugin-vue": "^2.3.1", "@vitejs/plugin-vue": "^2.3.1",
"vite": "^2.9.5" "vite": "^2.9.5"
}, },
@ -7786,10 +7786,10 @@
}, },
"packages/playwright-ct-vue2": { "packages/playwright-ct-vue2": {
"name": "@playwright/experimental-ct-vue2", "name": "@playwright/experimental-ct-vue2",
"version": "1.27.0-next", "version": "1.27.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/test": "1.27.0-next", "@playwright/test": "1.27.1",
"vite": "^2.9.5", "vite": "^2.9.5",
"vite-plugin-vue2": "^2.0.1" "vite-plugin-vue2": "^2.0.1"
}, },
@ -7801,11 +7801,11 @@
} }
}, },
"packages/playwright-firefox": { "packages/playwright-firefox": {
"version": "1.27.0-next", "version": "1.27.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7816,11 +7816,11 @@
}, },
"packages/playwright-test": { "packages/playwright-test": {
"name": "@playwright/test", "name": "@playwright/test",
"version": "1.27.0-next", "version": "1.27.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7830,11 +7830,11 @@
} }
}, },
"packages/playwright-webkit": { "packages/playwright-webkit": {
"version": "1.27.0-next", "version": "1.27.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8525,7 +8525,7 @@
"@playwright/experimental-ct-react": { "@playwright/experimental-ct-react": {
"version": "file:packages/playwright-ct-react", "version": "file:packages/playwright-ct-react",
"requires": { "requires": {
"@playwright/test": "1.27.0-next", "@playwright/test": "1.27.1",
"@vitejs/plugin-react": "^2.0.1", "@vitejs/plugin-react": "^2.0.1",
"vite": "^3.0.9" "vite": "^3.0.9"
}, },
@ -8734,7 +8734,7 @@
"@playwright/experimental-ct-solid": { "@playwright/experimental-ct-solid": {
"version": "file:packages/playwright-ct-solid", "version": "file:packages/playwright-ct-solid",
"requires": { "requires": {
"@playwright/test": "1.27.0-next", "@playwright/test": "1.27.1",
"solid-js": "^1.4.7", "solid-js": "^1.4.7",
"vite": "^3.0.0", "vite": "^3.0.0",
"vite-plugin-solid": "^2.3.0" "vite-plugin-solid": "^2.3.0"
@ -8919,7 +8919,7 @@
"@playwright/experimental-ct-svelte": { "@playwright/experimental-ct-svelte": {
"version": "file:packages/playwright-ct-svelte", "version": "file:packages/playwright-ct-svelte",
"requires": { "requires": {
"@playwright/test": "1.27.0-next", "@playwright/test": "1.27.1",
"@sveltejs/vite-plugin-svelte": "^1.0.1", "@sveltejs/vite-plugin-svelte": "^1.0.1",
"svelte": "^3.49.0", "svelte": "^3.49.0",
"vite": "^3.0.0" "vite": "^3.0.0"
@ -9105,7 +9105,7 @@
"@playwright/experimental-ct-vue": { "@playwright/experimental-ct-vue": {
"version": "file:packages/playwright-ct-vue", "version": "file:packages/playwright-ct-vue",
"requires": { "requires": {
"@playwright/test": "1.27.0-next", "@playwright/test": "1.27.1",
"@vitejs/plugin-vue": "^2.3.1", "@vitejs/plugin-vue": "^2.3.1",
"vite": "^2.9.5" "vite": "^2.9.5"
}, },
@ -9138,7 +9138,7 @@
"@playwright/experimental-ct-vue2": { "@playwright/experimental-ct-vue2": {
"version": "file:packages/playwright-ct-vue2", "version": "file:packages/playwright-ct-vue2",
"requires": { "requires": {
"@playwright/test": "1.27.0-next", "@playwright/test": "1.27.1",
"vite": "^2.9.5", "vite": "^2.9.5",
"vite-plugin-vue2": "^2.0.1", "vite-plugin-vue2": "^2.0.1",
"vue": "^2.6.14" "vue": "^2.6.14"
@ -9148,7 +9148,7 @@
"version": "file:packages/playwright-test", "version": "file:packages/playwright-test",
"requires": { "requires": {
"@types/node": "*", "@types/node": "*",
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
} }
}, },
"@rollup/pluginutils": { "@rollup/pluginutils": {
@ -11374,13 +11374,13 @@
"playwright": { "playwright": {
"version": "file:packages/playwright", "version": "file:packages/playwright",
"requires": { "requires": {
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
} }
}, },
"playwright-chromium": { "playwright-chromium": {
"version": "file:packages/playwright-chromium", "version": "file:packages/playwright-chromium",
"requires": { "requires": {
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
} }
}, },
"playwright-core": { "playwright-core": {
@ -11389,13 +11389,13 @@
"playwright-firefox": { "playwright-firefox": {
"version": "file:packages/playwright-firefox", "version": "file:packages/playwright-firefox",
"requires": { "requires": {
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
} }
}, },
"playwright-webkit": { "playwright-webkit": {
"version": "file:packages/playwright-webkit", "version": "file:packages/playwright-webkit",
"requires": { "requires": {
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
} }
}, },
"postcss": { "postcss": {

View file

@ -1,7 +1,7 @@
{ {
"name": "playwright-internal", "name": "playwright-internal",
"private": true, "private": true,
"version": "1.27.0-next", "version": "1.27.1",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -13,7 +13,7 @@
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {
"dtest": "cross-env PLAYWRIGHT_DOCKER=1 playwright docker --config=tests/library/playwright.config.ts --grep '@smoke'", "dtest": "cross-env PLAYWRIGHT_DOCKER=1 playwright test --config=tests/library/playwright.config.ts --grep '@smoke'",
"ctest": "playwright test --config=tests/library/playwright.config.ts --project=chromium", "ctest": "playwright test --config=tests/library/playwright.config.ts --project=chromium",
"ftest": "playwright test --config=tests/library/playwright.config.ts --project=firefox", "ftest": "playwright test --config=tests/library/playwright.config.ts --project=firefox",
"wtest": "playwright test --config=tests/library/playwright.config.ts --project=webkit", "wtest": "playwright test --config=tests/library/playwright.config.ts --project=webkit",

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-chromium", "name": "playwright-chromium",
"version": "1.27.0-next", "version": "1.27.1",
"description": "A high-level API to automate Chromium", "description": "A high-level API to automate Chromium",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -28,6 +28,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-core", "name": "playwright-core",
"version": "1.27.0-next", "version": "1.27.1",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",

View file

@ -329,11 +329,11 @@ if (!process.env.PW_LANG_NAME) {
require('playwright'); require('playwright');
hasPlaywrightPackage = true; hasPlaywrightPackage = true;
} catch {} } catch {}
const strayPackage = hasPlaywrightPackage ? 'playwright' : 'playwright-core';
console.error(wrapInASCIIBox([ console.error(wrapInASCIIBox([
`Playwright Test compatibility check failed:`, `Playwright Test integrity check failed:`,
`@playwright/test version '${pwTestVersion}' does not match ${hasPlaywrightPackage ? 'playwright' : 'playwright-core'} version '${pwCoreVersion}'!`, `You have @playwright/test version '${pwTestVersion}' and '${strayPackage}' version '${pwCoreVersion}' installed!`,
`To fix this either align the versions or only keep @playwright/test since it depends on playwright-core.`, `You probably added '${strayPackage}' into your package.json by accident, remove it and re-run 'npm install'`,
`If you still receive this error, execute 'npm ci' or delete 'node_modules' and do 'npm install' again.`,
].join('\n'), 1)); ].join('\n'), 1));
process.exit(1); process.exit(1);
} }

View file

@ -90,8 +90,8 @@ class ProtocolHandler {
this._controller.on(DebugController.Events.BrowsersChanged, browsers => { this._controller.on(DebugController.Events.BrowsersChanged, browsers => {
process.send!({ method: 'browsersChanged', params: { browsers } }); process.send!({ method: 'browsersChanged', params: { browsers } });
}); });
this._controller.on(DebugController.Events.InspectRequested, selector => { this._controller.on(DebugController.Events.InspectRequested, ({ selector, locators }) => {
process.send!({ method: 'inspectRequested', params: { selector } }); process.send!({ method: 'inspectRequested', params: { selector, locators } });
}); });
} }

View file

@ -396,7 +396,7 @@ export function setTestIdAttribute(attributeName: string) {
function getByAttributeTextSelector(attrName: string, text: string | RegExp, options?: { exact?: boolean }): string { function getByAttributeTextSelector(attrName: string, text: string | RegExp, options?: { exact?: boolean }): string {
if (!isString(text)) if (!isString(text))
return `internal:attr=[${attrName}=${text}]`; return `internal:attr=[${attrName}=${text}]`;
return `internal:attr=[${attrName}=${escapeForAttributeSelector(text)}${options?.exact ? 's' : 'i'}]`; return `internal:attr=[${attrName}=${escapeForAttributeSelector(text, options?.exact || false)}]`;
} }
export function getByTestIdSelector(testId: string): string { export function getByTestIdSelector(testId: string): string {
@ -439,7 +439,7 @@ export function getByRoleSelector(role: string, options: ByRoleOptions = {}): st
if (options.level !== undefined) if (options.level !== undefined)
props.push(['level', String(options.level)]); props.push(['level', String(options.level)]);
if (options.name !== undefined) if (options.name !== undefined)
props.push(['name', isString(options.name) ? escapeForAttributeSelector(options.name) : String(options.name)]); props.push(['name', isString(options.name) ? escapeForAttributeSelector(options.name, false) : String(options.name)]);
if (options.pressed !== undefined) if (options.pressed !== undefined)
props.push(['pressed', String(options.pressed)]); props.push(['pressed', String(options.pressed)]);
return `role=${role}${props.map(([n, v]) => `[${n}=${v}]`).join('')}`; return `role=${role}${props.map(([n, v]) => `[${n}=${v}]`).join('')}`;

View file

@ -75,12 +75,12 @@ async function deletePlaywrightImage() {
async function buildPlaywrightImage() { async function buildPlaywrightImage() {
await checkDockerEngineIsRunningOrDie(); await checkDockerEngineIsRunningOrDie();
const isDevelopmentMode = getPlaywrightVersion().includes('next');
let baseImageName = `mcr.microsoft.com/playwright:v${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`;
// 1. Build or pull base image. // 1. Build or pull base image.
if (isDevelopmentMode) { let baseImageName = process.env.PWTEST_DOCKER_BASE_IMAGE || '';
// Use our docker build scripts in development mode! if (!baseImageName) {
if (!process.env.PWTEST_DOCKER_BASE_IMAGE) { const isDevelopmentMode = getPlaywrightVersion().includes('next');
if (isDevelopmentMode) {
// Use our docker build scripts in development mode!
const arch = process.arch === 'arm64' ? '--arm64' : '--amd64'; const arch = process.arch === 'arm64' ? '--arm64' : '--amd64';
throw createStacklessError(utils.wrapInASCIIBox([ throw createStacklessError(utils.wrapInASCIIBox([
`You are in DEVELOPMENT mode!`, `You are in DEVELOPMENT mode!`,
@ -91,8 +91,7 @@ async function buildPlaywrightImage() {
` PWTEST_DOCKER_BASE_IMAGE=playwright:localbuild npx playwright docker build`, ` PWTEST_DOCKER_BASE_IMAGE=playwright:localbuild npx playwright docker build`,
].join('\n'), 1)); ].join('\n'), 1));
} }
baseImageName = process.env.PWTEST_DOCKER_BASE_IMAGE; baseImageName = `mcr.microsoft.com/playwright:v${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`;
} else {
const { code } = await spawnAsync('docker', ['pull', baseImageName], { stdio: 'inherit' }); const { code } = await spawnAsync('docker', ['pull', baseImageName], { stdio: 'inherit' });
if (code !== 0) if (code !== 0)
throw new Error('Failed to pull docker image!'); throw new Error('Failed to pull docker image!');
@ -286,6 +285,7 @@ export function addDockerCLI(program: Command) {
await buildPlaywrightImage(); await buildPlaywrightImage();
} catch (e) { } catch (e) {
console.error(e.stack ? e : e.message); console.error(e.stack ? e : e.message);
process.exit(1);
} }
}); });
@ -296,6 +296,7 @@ export function addDockerCLI(program: Command) {
await startPlaywrightContainer(); await startPlaywrightContainer();
} catch (e) { } catch (e) {
console.error(e.stack ? e : e.message); console.error(e.stack ? e : e.message);
process.exit(1);
} }
}); });
@ -306,6 +307,7 @@ export function addDockerCLI(program: Command) {
await stopAllPlaywrightContainers(); await stopAllPlaywrightContainers();
} catch (e) { } catch (e) {
console.error(e.stack ? e : e.message); console.error(e.stack ? e : e.message);
process.exit(1);
} }
}); });
@ -316,11 +318,12 @@ export function addDockerCLI(program: Command) {
await deletePlaywrightImage(); await deletePlaywrightImage();
} catch (e) { } catch (e) {
console.error(e.stack ? e : e.message); console.error(e.stack ? e : e.message);
process.exit(1);
} }
}); });
dockerCommand.command('install-server-deps', { hidden: true }) dockerCommand.command('install-server-deps', { hidden: true })
.description('delete docker image, if any') .description('install run-server dependencies')
.action(async function() { .action(async function() {
const { code } = await spawnAsync('bash', [path.join(__dirname, '..', '..', 'bin', 'container_install_deps.sh')], { stdio: 'inherit' }); const { code } = await spawnAsync('bash', [path.join(__dirname, '..', '..', 'bin', 'container_install_deps.sh')], { stdio: 'inherit' });
if (code !== 0) if (code !== 0)
@ -328,7 +331,7 @@ export function addDockerCLI(program: Command) {
}); });
dockerCommand.command('run-server', { hidden: true }) dockerCommand.command('run-server', { hidden: true })
.description('delete docker image, if any') .description('run playwright server')
.action(async function() { .action(async function() {
await spawnAsync('bash', [path.join(__dirname, '..', '..', 'bin', 'container_run_server.sh')], { stdio: 'inherit' }); await spawnAsync('bash', [path.join(__dirname, '..', '..', 'bin', 'container_run_server.sh')], { stdio: 'inherit' });
}); });

View file

@ -334,6 +334,7 @@ scheme.RecorderSource = tObject({
scheme.DebugControllerInitializer = tOptional(tObject({})); scheme.DebugControllerInitializer = tOptional(tObject({}));
scheme.DebugControllerInspectRequestedEvent = tObject({ scheme.DebugControllerInspectRequestedEvent = tObject({
selector: tString, selector: tString,
locators: tArray(tType('NameValue')),
}); });
scheme.DebugControllerBrowsersChangedEvent = tObject({ scheme.DebugControllerBrowsersChangedEvent = tObject({
browsers: tArray(tObject({ browsers: tArray(tObject({

View file

@ -23,6 +23,9 @@ import type { InstrumentationListener } from './instrumentation';
import type { Playwright } from './playwright'; import type { Playwright } from './playwright';
import { Recorder } from './recorder'; import { Recorder } from './recorder';
import { EmptyRecorderApp } from './recorder/recorderApp'; import { EmptyRecorderApp } from './recorder/recorderApp';
import { asLocator } from './isomorphic/locatorGenerators';
import type { Language } from './isomorphic/locatorGenerators';
import type { NameValue } from '../common/types';
const internalMetadata = serverSideCallMetadata(); const internalMetadata = serverSideCallMetadata();
@ -215,7 +218,8 @@ class InspectingRecorderApp extends EmptyRecorderApp {
} }
override async setSelector(selector: string): Promise<void> { override async setSelector(selector: string): Promise<void> {
this._debugController.emit(DebugController.Events.InspectRequested, selector); const locators: NameValue[] = ['javascript', 'python', 'java', 'csharp'].map(l => ({ name: l, value: asLocator(l as Language, selector) }));
this._debugController.emit(DebugController.Events.InspectRequested, { selector, locators });
} }
override async setSources(sources: Source[]): Promise<void> { override async setSources(sources: Source[]): Promise<void> {

View file

@ -28,8 +28,8 @@ export class DebugControllerDispatcher extends Dispatcher<DebugController, chann
this._object.on(DebugController.Events.BrowsersChanged, browsers => { this._object.on(DebugController.Events.BrowsersChanged, browsers => {
this._dispatchEvent('browsersChanged', { browsers }); this._dispatchEvent('browsersChanged', { browsers });
}); });
this._object.on(DebugController.Events.InspectRequested, selector => { this._object.on(DebugController.Events.InspectRequested, ({ selector, locators }) => {
this._dispatchEvent('inspectRequested', { selector }); this._dispatchEvent('inspectRequested', { selector, locators });
}); });
this._object.on(DebugController.Events.SourcesChanged, sources => { this._object.on(DebugController.Events.SourcesChanged, sources => {
this._dispatchEvent('sourcesChanged', { sources }); this._dispatchEvent('sourcesChanged', { sources });

View file

@ -17,6 +17,8 @@
import { stringifySelector } from '../isomorphic/selectorParser'; import { stringifySelector } from '../isomorphic/selectorParser';
import type { ParsedSelector } from '../isomorphic/selectorParser'; import type { ParsedSelector } from '../isomorphic/selectorParser';
import type { InjectedScript } from './injectedScript'; import type { InjectedScript } from './injectedScript';
import { asLocator } from '../isomorphic/locatorGenerators';
import type { Language } from '../isomorphic/locatorGenerators';
type HighlightEntry = { type HighlightEntry = {
targetElement: Element, targetElement: Element,
@ -35,6 +37,7 @@ export class Highlight {
private _isUnderTest: boolean; private _isUnderTest: boolean;
private _injectedScript: InjectedScript; private _injectedScript: InjectedScript;
private _rafRequest: number | undefined; private _rafRequest: number | undefined;
private _language: Language = 'javascript';
constructor(injectedScript: InjectedScript) { constructor(injectedScript: InjectedScript) {
this._injectedScript = injectedScript; this._injectedScript = injectedScript;
@ -102,6 +105,10 @@ export class Highlight {
document.documentElement.appendChild(this._glassPaneElement); document.documentElement.appendChild(this._glassPaneElement);
} }
setLanguage(language: Language) {
this._language = language;
}
runHighlightOnRaf(selector: ParsedSelector) { runHighlightOnRaf(selector: ParsedSelector) {
if (this._rafRequest) if (this._rafRequest)
cancelAnimationFrame(this._rafRequest); cancelAnimationFrame(this._rafRequest);
@ -145,7 +152,7 @@ export class Highlight {
color = '#dc6f6f7f'; color = '#dc6f6f7f';
else else
color = elements.length > 1 ? '#f6b26b7f' : '#6fa8dc7f'; color = elements.length > 1 ? '#f6b26b7f' : '#6fa8dc7f';
this._innerUpdateHighlight(elements, { color, tooltipText: selector }); this._innerUpdateHighlight(elements, { color, tooltipText: selector ? asLocator(this._language, selector) : '' });
} }
maskElements(elements: Element[]) { maskElements(elements: Element[]) {

View file

@ -94,7 +94,8 @@ class Recorder {
return; return;
} }
const { mode, actionPoint, actionSelector } = state; const { mode, actionPoint, actionSelector, language } = state;
this._highlight.setLanguage(language);
if (mode !== this._mode) { if (mode !== this._mode) {
this._mode = mode; this._mode = mode;
this._clearHighlight(); this._clearHighlight();

View file

@ -148,7 +148,7 @@ function buildCandidates(injectedScript: InjectedScript, element: Element, acces
const candidates: SelectorToken[] = []; const candidates: SelectorToken[] = [];
if (element.getAttribute('data-testid')) if (element.getAttribute('data-testid'))
candidates.push({ engine: 'internal:attr', selector: `[data-testid=${escapeForAttributeSelector(element.getAttribute('data-testid')!)}]`, score: 1 }); candidates.push({ engine: 'internal:attr', selector: `[data-testid=${escapeForAttributeSelector(element.getAttribute('data-testid')!, true)}]`, score: 1 });
for (const attr of ['data-test-id', 'data-test']) { for (const attr of ['data-test-id', 'data-test']) {
if (element.getAttribute(attr)) if (element.getAttribute(attr))
@ -158,7 +158,7 @@ function buildCandidates(injectedScript: InjectedScript, element: Element, acces
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
const input = element as HTMLInputElement | HTMLTextAreaElement; const input = element as HTMLInputElement | HTMLTextAreaElement;
if (input.placeholder) if (input.placeholder)
candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder)}]`, score: 3 }); candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder, false)}]`, score: 3 });
const label = input.labels?.[0]; const label = input.labels?.[0];
if (label) { if (label) {
const labelText = elementText(injectedScript._evaluator._cacheText, label).full.trim(); const labelText = elementText(injectedScript._evaluator._cacheText, label).full.trim();
@ -170,13 +170,13 @@ function buildCandidates(injectedScript: InjectedScript, element: Element, acces
if (ariaRole) { if (ariaRole) {
const ariaName = getElementAccessibleName(element, false, accessibleNameCache); const ariaName = getElementAccessibleName(element, false, accessibleNameCache);
if (ariaName) if (ariaName)
candidates.push({ engine: 'role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName)}]`, score: 3 }); candidates.push({ engine: 'role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: 3 });
else else
candidates.push({ engine: 'role', selector: ariaRole, score: 150 }); candidates.push({ engine: 'role', selector: ariaRole, score: 150 });
} }
if (element.getAttribute('alt') && ['APPLET', 'AREA', 'IMG', 'INPUT'].includes(element.nodeName)) if (element.getAttribute('alt') && ['APPLET', 'AREA', 'IMG', 'INPUT'].includes(element.nodeName))
candidates.push({ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(element.getAttribute('alt')!)}]`, score: 10 }); candidates.push({ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(element.getAttribute('alt')!, false)}]`, score: 10 });
if (element.getAttribute('name') && ['BUTTON', 'FORM', 'FIELDSET', 'FRAME', 'IFRAME', 'INPUT', 'KEYGEN', 'OBJECT', 'OUTPUT', 'SELECT', 'TEXTAREA', 'MAP', 'META', 'PARAM'].includes(element.nodeName)) if (element.getAttribute('name') && ['BUTTON', 'FORM', 'FIELDSET', 'FRAME', 'IFRAME', 'INPUT', 'KEYGEN', 'OBJECT', 'OUTPUT', 'SELECT', 'TEXTAREA', 'MAP', 'META', 'PARAM'].includes(element.nodeName))
candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[name=${quoteAttributeValue(element.getAttribute('name')!)}]`, score: 50 }); candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[name=${quoteAttributeValue(element.getAttribute('name')!)}]`, score: 50 });

View file

@ -19,12 +19,12 @@ import type { CSSComplexSelectorList } from '../isomorphic/cssParser';
import { parseAttributeSelector, parseSelector, stringifySelector } from '../isomorphic/selectorParser'; import { parseAttributeSelector, parseSelector, stringifySelector } from '../isomorphic/selectorParser';
import type { ParsedSelector } from '../isomorphic/selectorParser'; import type { ParsedSelector } from '../isomorphic/selectorParser';
type Language = 'javascript' | 'python' | 'java' | 'csharp'; export type Language = 'javascript' | 'python' | 'java' | 'csharp';
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text'; export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text';
export type LocatorBase = 'page' | 'locator' | 'frame-locator'; export type LocatorBase = 'page' | 'locator' | 'frame-locator';
export interface LocatorFactory { export interface LocatorFactory {
generateLocator(base: LocatorBase, kind: LocatorType, body: string, options?: { attrs?: Record<string, string | boolean>, hasText?: string, exact?: boolean }): string; generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options?: { attrs?: Record<string, string | boolean>, hasText?: string, exact?: boolean }): string;
} }
export function asLocator(lang: Language, selector: string, isFrameLocator: boolean = false): string { export function asLocator(lang: Language, selector: string, isFrameLocator: boolean = false): string {
@ -74,13 +74,14 @@ function innerAsLocator(factory: LocatorFactory, selector: string, isFrameLocato
if (part.name === 'internal:attr') { if (part.name === 'internal:attr') {
const attrSelector = parseAttributeSelector(part.body as string, true); const attrSelector = parseAttributeSelector(part.body as string, true);
const { name, value } = attrSelector.attributes[0]; const { name, value, caseSensitive } = attrSelector.attributes[0];
if (name === 'data-testid') { if (name === 'data-testid') {
tokens.push(factory.generateLocator(base, 'test-id', value)); tokens.push(factory.generateLocator(base, 'test-id', value));
continue; continue;
} }
const { exact, text } = detectExact(value); const text = value as string | RegExp;
const exact = !!caseSensitive;
if (name === 'placeholder') { if (name === 'placeholder') {
tokens.push(factory.generateLocator(base, 'placeholder', text, { exact })); tokens.push(factory.generateLocator(base, 'placeholder', text, { exact }));
continue; continue;
@ -104,8 +105,11 @@ function innerAsLocator(factory: LocatorFactory, selector: string, isFrameLocato
return tokens.join('.'); return tokens.join('.');
} }
function detectExact(text: string): { exact: boolean, text: string } { function detectExact(text: string): { exact?: boolean, text: string | RegExp } {
let exact = false; let exact = false;
const match = text.match(/^\/(.*)\/([igm]*)$/);
if (match)
return { text: new RegExp(match[1], match[2]) };
if (text.startsWith('"') && text.endsWith('"')) { if (text.startsWith('"') && text.endsWith('"')) {
text = JSON.parse(text); text = JSON.parse(text);
exact = true; exact = true;
@ -114,10 +118,10 @@ function detectExact(text: string): { exact: boolean, text: string } {
} }
export class JavaScriptLocatorFactory implements LocatorFactory { export class JavaScriptLocatorFactory implements LocatorFactory {
generateLocator(base: LocatorBase, kind: LocatorType, body: string, options: { attrs?: Record<string, string | boolean>, hasText?: string, exact?: boolean } = {}): string { generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: { attrs?: Record<string, string | boolean>, hasText?: string, exact?: boolean } = {}): string {
switch (kind) { switch (kind) {
case 'default': case 'default':
return `locator(${this.quote(body)})`; return `locator(${this.quote(body as string)})`;
case 'nth': case 'nth':
return `nth(${body})`; return `nth(${body})`;
case 'first': case 'first':
@ -129,11 +133,11 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
for (const [name, value] of Object.entries(options.attrs!)) for (const [name, value] of Object.entries(options.attrs!))
attrs.push(`${name}: ${typeof value === 'string' ? this.quote(value) : value}`); attrs.push(`${name}: ${typeof value === 'string' ? this.quote(value) : value}`);
const attrString = attrs.length ? `, { ${attrs.join(', ')} }` : ''; const attrString = attrs.length ? `, { ${attrs.join(', ')} }` : '';
return `getByRole(${this.quote(body)}${attrString})`; return `getByRole(${this.quote(body as string)}${attrString})`;
case 'has-text': case 'has-text':
return `locator(${this.quote(body)}, { hasText: ${this.quote(options.hasText!)} })`; return `locator(${this.quote(body as string)}, { hasText: ${this.quote(options.hasText!)} })`;
case 'test-id': case 'test-id':
return `getByTestId(${this.quote(body)})`; return `getByTestId(${this.quote(body as string)})`;
case 'text': case 'text':
return this.toCallWithExact('getByText', body, !!options.exact); return this.toCallWithExact('getByText', body, !!options.exact);
case 'alt': case 'alt':
@ -149,8 +153,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
} }
} }
private toCallWithExact(method: string, body: string, exact: boolean) { private toCallWithExact(method: string, body: string | RegExp, exact?: boolean) {
if (body.startsWith('/') && (body.endsWith('/') || body.endsWith('/i'))) if (isRegExp(body))
return `${method}(${body})`; return `${method}(${body})`;
return exact ? `${method}(${this.quote(body)}, { exact: true })` : `${method}(${this.quote(body)})`; return exact ? `${method}(${this.quote(body)}, { exact: true })` : `${method}(${this.quote(body)})`;
} }
@ -161,10 +165,10 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
} }
export class PythonLocatorFactory implements LocatorFactory { export class PythonLocatorFactory implements LocatorFactory {
generateLocator(base: LocatorBase, kind: LocatorType, body: string, options: { attrs?: Record<string, string | boolean>, hasText?: string, exact?: boolean } = {}): string { generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: { attrs?: Record<string, string | boolean>, hasText?: string, exact?: boolean } = {}): string {
switch (kind) { switch (kind) {
case 'default': case 'default':
return `locator(${this.quote(body)})`; return `locator(${this.quote(body as string)})`;
case 'nth': case 'nth':
return `nth(${body})`; return `nth(${body})`;
case 'first': case 'first':
@ -176,11 +180,11 @@ export class PythonLocatorFactory implements LocatorFactory {
for (const [name, value] of Object.entries(options.attrs!)) for (const [name, value] of Object.entries(options.attrs!))
attrs.push(`${toSnakeCase(name)}=${typeof value === 'string' ? this.quote(value) : value}`); attrs.push(`${toSnakeCase(name)}=${typeof value === 'string' ? this.quote(value) : value}`);
const attrString = attrs.length ? `, ${attrs.join(', ')}` : ''; const attrString = attrs.length ? `, ${attrs.join(', ')}` : '';
return `get_by_role(${this.quote(body)}${attrString})`; return `get_by_role(${this.quote(body as string)}${attrString})`;
case 'has-text': case 'has-text':
return `locator(${this.quote(body)}, has_text=${this.quote(options.hasText!)})`; return `locator(${this.quote(body as string)}, has_text=${this.quote(options.hasText!)})`;
case 'test-id': case 'test-id':
return `get_by_test_id(${this.quote(body)})`; return `get_by_test_id(${this.quote(body as string)})`;
case 'text': case 'text':
return this.toCallWithExact('get_by_text', body, !!options.exact); return this.toCallWithExact('get_by_text', body, !!options.exact);
case 'alt': case 'alt':
@ -196,11 +200,10 @@ export class PythonLocatorFactory implements LocatorFactory {
} }
} }
private toCallWithExact(method: string, body: string, exact: boolean) { private toCallWithExact(method: string, body: string | RegExp, exact: boolean) {
if (body.startsWith('/') && (body.endsWith('/') || body.endsWith('/i'))) { if (isRegExp(body)) {
const regex = body.substring(1, body.lastIndexOf('/')); const suffix = body.flags.includes('i') ? ', re.IGNORECASE' : '';
const suffix = body.endsWith('i') ? ', re.IGNORECASE' : ''; return `${method}(re.compile(r${this.quote(body.source)}${suffix}))`;
return `${method}(re.compile(r${this.quote(regex)}${suffix}))`;
} }
if (exact) if (exact)
return `${method}(${this.quote(body)}, exact=true)`; return `${method}(${this.quote(body)}, exact=true)`;
@ -213,7 +216,7 @@ export class PythonLocatorFactory implements LocatorFactory {
} }
export class JavaLocatorFactory implements LocatorFactory { export class JavaLocatorFactory implements LocatorFactory {
generateLocator(base: LocatorBase, kind: LocatorType, body: string, options: { attrs?: Record<string, string | boolean>, hasText?: string, exact?: boolean } = {}): string { generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: { attrs?: Record<string, string | boolean>, hasText?: string, exact?: boolean } = {}): string {
let clazz: string; let clazz: string;
switch (base) { switch (base) {
case 'page': clazz = 'Page'; break; case 'page': clazz = 'Page'; break;
@ -222,7 +225,7 @@ export class JavaLocatorFactory implements LocatorFactory {
} }
switch (kind) { switch (kind) {
case 'default': case 'default':
return `locator(${this.quote(body)})`; return `locator(${this.quote(body as string)})`;
case 'nth': case 'nth':
return `nth(${body})`; return `nth(${body})`;
case 'first': case 'first':
@ -234,11 +237,11 @@ export class JavaLocatorFactory implements LocatorFactory {
for (const [name, value] of Object.entries(options.attrs!)) for (const [name, value] of Object.entries(options.attrs!))
attrs.push(`.set${toTitleCase(name)}(${typeof value === 'string' ? this.quote(value) : value})`); attrs.push(`.set${toTitleCase(name)}(${typeof value === 'string' ? this.quote(value) : value})`);
const attrString = attrs.length ? `, new ${clazz}.GetByRoleOptions()${attrs.join('')}` : ''; const attrString = attrs.length ? `, new ${clazz}.GetByRoleOptions()${attrs.join('')}` : '';
return `getByRole(${this.quote(body)}${attrString})`; return `getByRole(AriaRole.${toSnakeCase(body as string).toUpperCase()}${attrString})`;
case 'has-text': case 'has-text':
return `locator(${this.quote(body)}, new ${clazz}.LocatorOptions().setHasText(${this.quote(options.hasText!)}))`; return `locator(${this.quote(body as string)}, new ${clazz}.LocatorOptions().setHasText(${this.quote(options.hasText!)}))`;
case 'test-id': case 'test-id':
return `getByTestId(${this.quote(body)})`; return `getByTestId(${this.quote(body as string)})`;
case 'text': case 'text':
return this.toCallWithExact(clazz, 'getByText', body, !!options.exact); return this.toCallWithExact(clazz, 'getByText', body, !!options.exact);
case 'alt': case 'alt':
@ -254,11 +257,10 @@ export class JavaLocatorFactory implements LocatorFactory {
} }
} }
private toCallWithExact(clazz: string, method: string, body: string, exact: boolean) { private toCallWithExact(clazz: string, method: string, body: string | RegExp, exact: boolean) {
if (body.startsWith('/') && (body.endsWith('/') || body.endsWith('/i'))) { if (isRegExp(body)) {
const regex = body.substring(1, body.lastIndexOf('/')); const suffix = body.flags.includes('i') ? ', Pattern.CASE_INSENSITIVE' : '';
const suffix = body.endsWith('i') ? ', Pattern.CASE_INSENSITIVE' : ''; return `${method}(Pattern.compile(${this.quote(body.source)}${suffix}))`;
return `${method}(Pattern.compile(${this.quote(regex)}${suffix}))`;
} }
if (exact) if (exact)
return `${method}(${this.quote(body)}, new ${clazz}.${toTitleCase(method)}Options().setExact(exact))`; return `${method}(${this.quote(body)}, new ${clazz}.${toTitleCase(method)}Options().setExact(exact))`;
@ -271,10 +273,10 @@ export class JavaLocatorFactory implements LocatorFactory {
} }
export class CSharpLocatorFactory implements LocatorFactory { export class CSharpLocatorFactory implements LocatorFactory {
generateLocator(base: LocatorBase, kind: LocatorType, body: string, options: { attrs?: Record<string, string | boolean>, hasText?: string, exact?: boolean } = {}): string { generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: { attrs?: Record<string, string | boolean>, hasText?: string, exact?: boolean } = {}): string {
switch (kind) { switch (kind) {
case 'default': case 'default':
return `Locator(${this.quote(body)})`; return `Locator(${this.quote(body as string)})`;
case 'nth': case 'nth':
return `Nth(${body})`; return `Nth(${body})`;
case 'first': case 'first':
@ -283,14 +285,16 @@ export class CSharpLocatorFactory implements LocatorFactory {
return `Last`; return `Last`;
case 'role': case 'role':
const attrs: string[] = []; const attrs: string[] = [];
for (const [name, value] of Object.entries(options.attrs!)) for (const [name, value] of Object.entries(options.attrs!)) {
attrs.push(`${toTitleCase(name)} = ${typeof value === 'string' ? this.quote(value) : value}`); const optionKey = name === 'name' ? 'NameString' : toTitleCase(name);
const attrString = attrs.length ? `, new () { ${attrs.join(', ')} }` : ''; attrs.push(`${optionKey} = ${typeof value === 'string' ? this.quote(value) : value}`);
return `GetByRole(${this.quote(body)}${attrString})`; }
const attrString = attrs.length ? `, new() { ${attrs.join(', ')} }` : '';
return `GetByRole(AriaRole.${toTitleCase(body as string)}${attrString})`;
case 'has-text': case 'has-text':
return `Locator(${this.quote(body)}, new () { HasTextString: ${this.quote(options.hasText!)} })`; return `Locator(${this.quote(body as string)}, new() { HasTextString: ${this.quote(options.hasText!)} })`;
case 'test-id': case 'test-id':
return `GetByTestId(${this.quote(body)})`; return `GetByTestId(${this.quote(body as string)})`;
case 'text': case 'text':
return this.toCallWithExact('GetByText', body, !!options.exact); return this.toCallWithExact('GetByText', body, !!options.exact);
case 'alt': case 'alt':
@ -306,14 +310,13 @@ export class CSharpLocatorFactory implements LocatorFactory {
} }
} }
private toCallWithExact(method: string, body: string, exact: boolean) { private toCallWithExact(method: string, body: string | RegExp, exact: boolean) {
if (body.startsWith('/') && (body.endsWith('/') || body.endsWith('/i'))) { if (isRegExp(body)) {
const regex = body.substring(1, body.lastIndexOf('/')); const suffix = body.flags.includes('i') ? ', RegexOptions.IgnoreCase' : '';
const suffix = body.endsWith('i') ? ', RegexOptions.IgnoreCase' : ''; return `${method}(new Regex(${this.quote(body.source)}${suffix}))`;
return `${method}(new Regex(${this.quote(regex)}${suffix}))`;
} }
if (exact) if (exact)
return `${method}(${this.quote(body)}, new () { Exact: true })`; return `${method}(${this.quote(body)}, new() { Exact: true })`;
return `${method}(${this.quote(body)})`; return `${method}(${this.quote(body)})`;
} }
@ -328,3 +331,7 @@ const generators: Record<Language, LocatorFactory> = {
java: new JavaLocatorFactory(), java: new JavaLocatorFactory(),
csharp: new CSharpLocatorFactory(), csharp: new CSharpLocatorFactory(),
}; };
export function isRegExp(obj: any): obj is RegExp {
return obj instanceof RegExp;
}

View file

@ -40,7 +40,7 @@ import { metadataToCallLog } from './recorder/recorderUtils';
import { Debugger } from './debugger'; import { Debugger } from './debugger';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { raceAgainstTimeout } from '../utils/timeoutRunner'; import { raceAgainstTimeout } from '../utils/timeoutRunner';
import type { LanguageGenerator } from './recorder/language'; import type { Language, LanguageGenerator } from './recorder/language';
type BindingSource = { frame: Frame, page: Page }; type BindingSource = { frame: Frame, page: Page };
@ -59,6 +59,7 @@ export class Recorder implements InstrumentationListener {
private _handleSIGINT: boolean | undefined; private _handleSIGINT: boolean | undefined;
private _recorderAppFactory: (recorder: Recorder) => Promise<IRecorderApp>; private _recorderAppFactory: (recorder: Recorder) => Promise<IRecorderApp>;
private _omitCallTracking = false; private _omitCallTracking = false;
private _currentLanguage: Language;
static showInspector(context: BrowserContext) { static showInspector(context: BrowserContext) {
Recorder.show(context, {}).catch(() => {}); Recorder.show(context, {}).catch(() => {});
@ -83,6 +84,7 @@ export class Recorder implements InstrumentationListener {
this._debugger = Debugger.lookup(context)!; this._debugger = Debugger.lookup(context)!;
this._handleSIGINT = params.handleSIGINT; this._handleSIGINT = params.handleSIGINT;
context.instrumentation.addListener(this, context); context.instrumentation.addListener(this, context);
this._currentLanguage = this._contextRecorder.languageName();
} }
private static async defaultRecorderAppFactory(recorder: Recorder) { private static async defaultRecorderAppFactory(recorder: Recorder) {
@ -111,6 +113,11 @@ export class Recorder implements InstrumentationListener {
this._debugger.resume(true); this._debugger.resume(true);
return; return;
} }
if (data.event === 'fileChanged') {
this._currentLanguage = this._contextRecorder.languageName(data.params.file);
this._refreshOverlay();
return;
}
if (data.event === 'resume') { if (data.event === 'resume') {
this._debugger.resume(false); this._debugger.resume(false);
return; return;
@ -155,6 +162,7 @@ export class Recorder implements InstrumentationListener {
mode: this._mode, mode: this._mode,
actionPoint, actionPoint,
actionSelector, actionSelector,
language: this._currentLanguage
}; };
return uiState; return uiState;
}); });
@ -381,6 +389,14 @@ class ContextRecorder extends EventEmitter {
this._generator?.restart(); this._generator?.restart();
} }
languageName(id?: string): Language {
for (const lang of this._orderedLanguages) {
if (!id || lang.id === id)
return lang.highlighter;
}
return 'javascript';
}
async install() { async install() {
this._context.on(BrowserContext.Events.Page, page => this._onPage(page)); this._context.on(BrowserContext.Events.Page, page => this._onPage(page));
for (const page of this._context.pages()) for (const page of this._context.pages())

View file

@ -15,7 +15,7 @@
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../..';
import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
import { sanitizeDeviceOptions, toSignalMap } from './language'; import { sanitizeDeviceOptions, toSignalMap } from './language';
import type { ActionInContext } from './codeGenerator'; import type { ActionInContext } from './codeGenerator';
import type { Action } from './recorderActions'; import type { Action } from './recorderActions';
@ -31,7 +31,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
id: string; id: string;
groupName = '.NET C#'; groupName = '.NET C#';
name: string; name: string;
highlighter = 'csharp'; highlighter = 'csharp' as Language;
_mode: CSharpLanguageMode; _mode: CSharpLanguageMode;
constructor(mode: CSharpLanguageMode) { constructor(mode: CSharpLanguageMode) {

View file

@ -15,7 +15,7 @@
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../..';
import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
import { toSignalMap } from './language'; import { toSignalMap } from './language';
import type { ActionInContext } from './codeGenerator'; import type { ActionInContext } from './codeGenerator';
import type { Action } from './recorderActions'; import type { Action } from './recorderActions';
@ -30,7 +30,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
id = 'java'; id = 'java';
groupName = 'Java'; groupName = 'Java';
name = 'Library'; name = 'Library';
highlighter = 'java'; highlighter = 'java' as Language;
generateAction(actionInContext: ActionInContext): string { generateAction(actionInContext: ActionInContext): string {
const action = actionInContext.action; const action = actionInContext.action;

View file

@ -15,7 +15,7 @@
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../..';
import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
import { sanitizeDeviceOptions, toSignalMap } from './language'; import { sanitizeDeviceOptions, toSignalMap } from './language';
import type { ActionInContext } from './codeGenerator'; import type { ActionInContext } from './codeGenerator';
import type { Action } from './recorderActions'; import type { Action } from './recorderActions';
@ -29,7 +29,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
id: string; id: string;
groupName = 'Node.js'; groupName = 'Node.js';
name: string; name: string;
highlighter = 'javascript'; highlighter = 'javascript' as Language;
private _isTest: boolean; private _isTest: boolean;
constructor(isTest: boolean) { constructor(isTest: boolean) {

View file

@ -15,8 +15,10 @@
*/ */
import type { BrowserContextOptions, LaunchOptions } from '../../..'; import type { BrowserContextOptions, LaunchOptions } from '../../..';
import type { Language } from '../isomorphic/locatorGenerators';
import type { ActionInContext } from './codeGenerator'; import type { ActionInContext } from './codeGenerator';
import type { Action, DialogSignal, DownloadSignal, NavigationSignal, PopupSignal } from './recorderActions'; import type { Action, DialogSignal, DownloadSignal, NavigationSignal, PopupSignal } from './recorderActions';
export type { Language } from '../isomorphic/locatorGenerators';
export type LanguageGeneratorOptions = { export type LanguageGeneratorOptions = {
browserName: string; browserName: string;
@ -33,7 +35,7 @@ export interface LanguageGenerator {
id: string; id: string;
groupName: string; groupName: string;
name: string; name: string;
highlighter: string; highlighter: Language;
generateHeader(options: LanguageGeneratorOptions): string; generateHeader(options: LanguageGeneratorOptions): string;
generateAction(actionInContext: ActionInContext): string; generateAction(actionInContext: ActionInContext): string;
generateFooter(saveStorage: string | undefined): string; generateFooter(saveStorage: string | undefined): string;

View file

@ -15,7 +15,7 @@
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../..';
import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
import { sanitizeDeviceOptions, toSignalMap } from './language'; import { sanitizeDeviceOptions, toSignalMap } from './language';
import type { ActionInContext } from './codeGenerator'; import type { ActionInContext } from './codeGenerator';
import type { Action } from './recorderActions'; import type { Action } from './recorderActions';
@ -29,7 +29,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
id: string; id: string;
groupName = 'Python'; groupName = 'Python';
name: string; name: string;
highlighter = 'python'; highlighter = 'python' as Language;
private _awaitPrefix: '' | 'await '; private _awaitPrefix: '' | 'await ';
private _asyncPrefix: '' | 'async '; private _asyncPrefix: '' | 'async ';

View file

@ -32,8 +32,7 @@ export function toTitleCase(name: string) {
} }
export function toSnakeCase(name: string): string { export function toSnakeCase(name: string): string {
const toSnakeCaseRegex = /((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))/g; return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
return name.replace(toSnakeCaseRegex, `_$1`).toLowerCase();
} }
export function cssEscape(s: string): string { export function cssEscape(s: string): string {
@ -69,14 +68,14 @@ export function escapeForTextSelector(text: string | RegExp, exact: boolean, cas
if (exact) if (exact)
return '"' + text.replace(/["]/g, '\\"') + '"'; return '"' + text.replace(/["]/g, '\\"') + '"';
if (text.includes('"') || text.includes('>>') || text[0] === '/') if (text.includes('"') || text.includes('>>') || text[0] === '/')
return `/${escapeForRegex(text).replace(/\s+/, '\\s+')}/` + (caseSensitive ? '' : 'i'); return `/${escapeForRegex(text).replace(/\s+/g, '\\s+')}/` + (caseSensitive ? '' : 'i');
return text; return text;
} }
export function escapeForAttributeSelector(value: string): string { export function escapeForAttributeSelector(value: string, exact: boolean): string {
// TODO: this should actually be // TODO: this should actually be
// cssEscape(value).replace(/\\ /g, ' ') // cssEscape(value).replace(/\\ /g, ' ')
// However, our attribute selectors do not conform to CSS parsing spec, // However, our attribute selectors do not conform to CSS parsing spec,
// so we escape them differently. // so we escape them differently.
return `"${value.replace(/["]/g, '\\"')}"`; return `"${value.replace(/["]/g, '\\"')}"${exact ? '' : 'i'}`;
} }

View file

@ -2457,7 +2457,8 @@ export interface Page {
*/ */
getByAltText(text: string|RegExp, options?: { getByAltText(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -2476,7 +2477,8 @@ export interface Page {
*/ */
getByLabel(text: string|RegExp, options?: { getByLabel(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -2494,7 +2496,8 @@ export interface Page {
*/ */
getByPlaceholder(text: string|RegExp, options?: { getByPlaceholder(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -2512,7 +2515,7 @@ export interface Page {
* @param role Required aria role. * @param role Required aria role.
* @param options * @param options
*/ */
getByRole(role: string, options?: { getByRole(role: "alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"|"caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"|"deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"|"group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"|"menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"|"presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|"searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"|"tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem", options?: {
/** /**
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for * An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for
* checked are `true`, `false` and `"mixed"`. * checked are `true`, `false` and `"mixed"`.
@ -2583,13 +2586,46 @@ export interface Page {
getByTestId(testId: string): Locator; getByTestId(testId: string): Locator;
/** /**
* Allows locating elements that contain given text. * Allows locating elements that contain given text. Consider the following DOM structure:
*
* ```html
* <div>Hello <span>world</span></div>
* <div>Hello</div>
* ```
*
* You can locate by text substring, exact string, or a regular expression:
*
* ```js
* // Matches <span>
* page.getByText('world')
*
* // Matches first <div>
* page.getByText('Hello world')
*
* // Matches second <div>
* page.getByText('Hello', { exact: true })
*
* // Matches both <div>s
* page.getByText(/Hello/)
*
* // Matches second <div>
* page.getByText(/^hello$/i)
* ```
*
* See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) that allows to match
* by another criteria, like an accessible role, and then filter by the text content.
*
* > NOTE: Matching by text always normalizes whitespace, even with exact match. For example, it turns multiple spaces into
* one, turns line breaks into spaces and ignores leading and trailing whitespace.
* > NOTE: Input elements of the type `button` and `submit` are matched by their `value` instead of the text content. For
* example, locating by text `"Log in"` matches `<input type=button value="Log in">`.
* @param text Text to locate the element for. * @param text Text to locate the element for.
* @param options * @param options
*/ */
getByText(text: string|RegExp, options?: { getByText(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -2606,7 +2642,8 @@ export interface Page {
*/ */
getByTitle(text: string|RegExp, options?: { getByTitle(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -5543,7 +5580,8 @@ export interface Frame {
*/ */
getByAltText(text: string|RegExp, options?: { getByAltText(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -5562,7 +5600,8 @@ export interface Frame {
*/ */
getByLabel(text: string|RegExp, options?: { getByLabel(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -5580,7 +5619,8 @@ export interface Frame {
*/ */
getByPlaceholder(text: string|RegExp, options?: { getByPlaceholder(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -5598,7 +5638,7 @@ export interface Frame {
* @param role Required aria role. * @param role Required aria role.
* @param options * @param options
*/ */
getByRole(role: string, options?: { getByRole(role: "alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"|"caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"|"deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"|"group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"|"menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"|"presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|"searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"|"tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem", options?: {
/** /**
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for * An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for
* checked are `true`, `false` and `"mixed"`. * checked are `true`, `false` and `"mixed"`.
@ -5669,13 +5709,46 @@ export interface Frame {
getByTestId(testId: string): Locator; getByTestId(testId: string): Locator;
/** /**
* Allows locating elements that contain given text. * Allows locating elements that contain given text. Consider the following DOM structure:
*
* ```html
* <div>Hello <span>world</span></div>
* <div>Hello</div>
* ```
*
* You can locate by text substring, exact string, or a regular expression:
*
* ```js
* // Matches <span>
* page.getByText('world')
*
* // Matches first <div>
* page.getByText('Hello world')
*
* // Matches second <div>
* page.getByText('Hello', { exact: true })
*
* // Matches both <div>s
* page.getByText(/Hello/)
*
* // Matches second <div>
* page.getByText(/^hello$/i)
* ```
*
* See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) that allows to match
* by another criteria, like an accessible role, and then filter by the text content.
*
* > NOTE: Matching by text always normalizes whitespace, even with exact match. For example, it turns multiple spaces into
* one, turns line breaks into spaces and ignores leading and trailing whitespace.
* > NOTE: Input elements of the type `button` and `submit` are matched by their `value` instead of the text content. For
* example, locating by text `"Log in"` matches `<input type=button value="Log in">`.
* @param text Text to locate the element for. * @param text Text to locate the element for.
* @param options * @param options
*/ */
getByText(text: string|RegExp, options?: { getByText(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -5692,7 +5765,8 @@ export interface Frame {
*/ */
getByTitle(text: string|RegExp, options?: { getByTitle(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -9966,7 +10040,8 @@ export interface Locator {
*/ */
getByAltText(text: string|RegExp, options?: { getByAltText(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -9985,7 +10060,8 @@ export interface Locator {
*/ */
getByLabel(text: string|RegExp, options?: { getByLabel(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -10003,7 +10079,8 @@ export interface Locator {
*/ */
getByPlaceholder(text: string|RegExp, options?: { getByPlaceholder(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -10021,7 +10098,7 @@ export interface Locator {
* @param role Required aria role. * @param role Required aria role.
* @param options * @param options
*/ */
getByRole(role: string, options?: { getByRole(role: "alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"|"caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"|"deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"|"group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"|"menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"|"presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|"searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"|"tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem", options?: {
/** /**
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for * An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for
* checked are `true`, `false` and `"mixed"`. * checked are `true`, `false` and `"mixed"`.
@ -10092,13 +10169,46 @@ export interface Locator {
getByTestId(testId: string): Locator; getByTestId(testId: string): Locator;
/** /**
* Allows locating elements that contain given text. * Allows locating elements that contain given text. Consider the following DOM structure:
*
* ```html
* <div>Hello <span>world</span></div>
* <div>Hello</div>
* ```
*
* You can locate by text substring, exact string, or a regular expression:
*
* ```js
* // Matches <span>
* page.getByText('world')
*
* // Matches first <div>
* page.getByText('Hello world')
*
* // Matches second <div>
* page.getByText('Hello', { exact: true })
*
* // Matches both <div>s
* page.getByText(/Hello/)
*
* // Matches second <div>
* page.getByText(/^hello$/i)
* ```
*
* See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) that allows to match
* by another criteria, like an accessible role, and then filter by the text content.
*
* > NOTE: Matching by text always normalizes whitespace, even with exact match. For example, it turns multiple spaces into
* one, turns line breaks into spaces and ignores leading and trailing whitespace.
* > NOTE: Input elements of the type `button` and `submit` are matched by their `value` instead of the text content. For
* example, locating by text `"Log in"` matches `<input type=button value="Log in">`.
* @param text Text to locate the element for. * @param text Text to locate the element for.
* @param options * @param options
*/ */
getByText(text: string|RegExp, options?: { getByText(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -10115,7 +10225,8 @@ export interface Locator {
*/ */
getByTitle(text: string|RegExp, options?: { getByTitle(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -15221,7 +15332,8 @@ export interface FrameLocator {
*/ */
getByAltText(text: string|RegExp, options?: { getByAltText(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -15240,7 +15352,8 @@ export interface FrameLocator {
*/ */
getByLabel(text: string|RegExp, options?: { getByLabel(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -15258,7 +15371,8 @@ export interface FrameLocator {
*/ */
getByPlaceholder(text: string|RegExp, options?: { getByPlaceholder(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -15276,7 +15390,7 @@ export interface FrameLocator {
* @param role Required aria role. * @param role Required aria role.
* @param options * @param options
*/ */
getByRole(role: string, options?: { getByRole(role: "alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"|"caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"|"deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"|"group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"|"menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"|"presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|"searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"|"tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem", options?: {
/** /**
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for * An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for
* checked are `true`, `false` and `"mixed"`. * checked are `true`, `false` and `"mixed"`.
@ -15347,13 +15461,46 @@ export interface FrameLocator {
getByTestId(testId: string): Locator; getByTestId(testId: string): Locator;
/** /**
* Allows locating elements that contain given text. * Allows locating elements that contain given text. Consider the following DOM structure:
*
* ```html
* <div>Hello <span>world</span></div>
* <div>Hello</div>
* ```
*
* You can locate by text substring, exact string, or a regular expression:
*
* ```js
* // Matches <span>
* page.getByText('world')
*
* // Matches first <div>
* page.getByText('Hello world')
*
* // Matches second <div>
* page.getByText('Hello', { exact: true })
*
* // Matches both <div>s
* page.getByText(/Hello/)
*
* // Matches second <div>
* page.getByText(/^hello$/i)
* ```
*
* See also [locator.filter([options])](https://playwright.dev/docs/api/class-locator#locator-filter) that allows to match
* by another criteria, like an accessible role, and then filter by the text content.
*
* > NOTE: Matching by text always normalizes whitespace, even with exact match. For example, it turns multiple spaces into
* one, turns line breaks into spaces and ignores leading and trailing whitespace.
* > NOTE: Input elements of the type `button` and `submit` are matched by their `value` instead of the text content. For
* example, locating by text `"Log in"` matches `<input type=button value="Log in">`.
* @param text Text to locate the element for. * @param text Text to locate the element for.
* @param options * @param options
*/ */
getByText(text: string|RegExp, options?: { getByText(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;
@ -15370,7 +15517,8 @@ export interface FrameLocator {
*/ */
getByTitle(text: string|RegExp, options?: { getByTitle(text: string|RegExp, options?: {
/** /**
* Whether to find an exact match: case-sensitive and whole-string. Default to false. * Whether to find an exact match: case-sensitive and whole-string. Default to false. Ignored when locating by a regular
* expression. Note that exact match still trims whitespace.
*/ */
exact?: boolean; exact?: boolean;
}): Locator; }): Locator;

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-react", "name": "@playwright/experimental-ct-react",
"version": "1.27.0-next", "version": "1.27.1",
"description": "Playwright Component Testing for React", "description": "Playwright Component Testing for React",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -27,7 +27,7 @@
}, },
"dependencies": { "dependencies": {
"@vitejs/plugin-react": "^2.0.1", "@vitejs/plugin-react": "^2.0.1",
"@playwright/test": "1.27.0-next", "@playwright/test": "1.27.1",
"vite": "^3.0.9" "vite": "^3.0.9"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-solid", "name": "@playwright/experimental-ct-solid",
"version": "1.27.0-next", "version": "1.27.1",
"description": "Playwright Component Testing for Solid", "description": "Playwright Component Testing for Solid",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -28,7 +28,7 @@
"dependencies": { "dependencies": {
"vite": "^3.0.0", "vite": "^3.0.0",
"vite-plugin-solid": "^2.3.0", "vite-plugin-solid": "^2.3.0",
"@playwright/test": "1.27.0-next" "@playwright/test": "1.27.1"
}, },
"devDependencies": { "devDependencies": {
"solid-js": "^1.4.7" "solid-js": "^1.4.7"

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-svelte", "name": "@playwright/experimental-ct-svelte",
"version": "1.27.0-next", "version": "1.27.1",
"description": "Playwright Component Testing for Svelte", "description": "Playwright Component Testing for Svelte",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -26,7 +26,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/test": "1.27.0-next", "@playwright/test": "1.27.1",
"@sveltejs/vite-plugin-svelte": "^1.0.1", "@sveltejs/vite-plugin-svelte": "^1.0.1",
"vite": "^3.0.0" "vite": "^3.0.0"
}, },

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-vue", "name": "@playwright/experimental-ct-vue",
"version": "1.27.0-next", "version": "1.27.1",
"description": "Playwright Component Testing for Vue", "description": "Playwright Component Testing for Vue",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -27,7 +27,7 @@
}, },
"dependencies": { "dependencies": {
"@vitejs/plugin-vue": "^2.3.1", "@vitejs/plugin-vue": "^2.3.1",
"@playwright/test": "1.27.0-next", "@playwright/test": "1.27.1",
"vite": "^2.9.5" "vite": "^2.9.5"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-vue2", "name": "@playwright/experimental-ct-vue2",
"version": "1.27.0-next", "version": "1.27.1",
"description": "Playwright Component Testing for Vue2", "description": "Playwright Component Testing for Vue2",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -26,7 +26,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/test": "1.27.0-next", "@playwright/test": "1.27.1",
"vite": "^2.9.5", "vite": "^2.9.5",
"vite-plugin-vue2": "^2.0.1" "vite-plugin-vue2": "^2.0.1"
}, },

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-firefox", "name": "playwright-firefox",
"version": "1.27.0-next", "version": "1.27.1",
"description": "A high-level API to automate Firefox", "description": "A high-level API to automate Firefox",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -28,6 +28,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/test", "name": "@playwright/test",
"version": "1.27.0-next", "version": "1.27.1",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -34,6 +34,6 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
} }
} }

View file

@ -23,7 +23,7 @@ import { transformHook, resolveHook, belongsToNodeModules } from './transform';
async function resolve(specifier: string, context: { parentURL?: string }, defaultResolve: Function) { async function resolve(specifier: string, context: { parentURL?: string }, defaultResolve: Function) {
if (context.parentURL && context.parentURL.startsWith('file://')) { if (context.parentURL && context.parentURL.startsWith('file://')) {
const filename = url.fileURLToPath(context.parentURL); const filename = url.fileURLToPath(context.parentURL);
const resolved = resolveHook(filename, specifier); const resolved = resolveHook(true, filename, specifier);
if (resolved !== undefined) if (resolved !== undefined)
specifier = url.pathToFileURL(resolved).toString(); specifier = url.pathToFileURL(resolved).toString();
} }

View file

@ -579,7 +579,9 @@ type ParsedStackTrace = {
apiName: string; apiName: string;
}; };
export function normalizeVideoMode(video: VideoMode | 'retry-with-video' | { mode: VideoMode }) { export function normalizeVideoMode(video: VideoMode | 'retry-with-video' | { mode: VideoMode } | undefined): VideoMode {
if (!video)
return 'off';
let videoMode = typeof video === 'string' ? video : video.mode; let videoMode = typeof video === 'string' ? video : video.mode;
if (videoMode === 'retry-with-video') if (videoMode === 'retry-with-video')
videoMode = 'on-first-retry'; videoMode = 'on-first-retry';
@ -590,7 +592,9 @@ export function shouldCaptureVideo(videoMode: VideoMode, testInfo: TestInfo) {
return (videoMode === 'on' || videoMode === 'retain-on-failure' || (videoMode === 'on-first-retry' && testInfo.retry === 1)); return (videoMode === 'on' || videoMode === 'retain-on-failure' || (videoMode === 'on-first-retry' && testInfo.retry === 1));
} }
export function normalizeTraceMode(trace: TraceMode | 'retry-with-trace' | { mode: TraceMode }) { export function normalizeTraceMode(trace: TraceMode | 'retry-with-trace' | { mode: TraceMode } | undefined): TraceMode {
if (!trace)
return 'off';
let traceMode = typeof trace === 'string' ? trace : trace.mode; let traceMode = typeof trace === 'string' ? trace : trace.mode;
if (traceMode === 'retry-with-trace') if (traceMode === 'retry-with-trace')
traceMode = 'on-first-retry'; traceMode = 'on-first-retry';

View file

@ -92,7 +92,7 @@ const scriptPreprocessor = process.env.PW_TEST_SOURCE_TRANSFORM ?
require(process.env.PW_TEST_SOURCE_TRANSFORM) : undefined; require(process.env.PW_TEST_SOURCE_TRANSFORM) : undefined;
const builtins = new Set(Module.builtinModules); const builtins = new Set(Module.builtinModules);
export function resolveHook(filename: string, specifier: string): string | undefined { export function resolveHook(isModule: boolean, filename: string, specifier: string): string | undefined {
if (builtins.has(specifier)) if (builtins.has(specifier))
return; return;
const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx'); const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx');
@ -132,11 +132,21 @@ export function resolveHook(filename: string, specifier: string): string | undef
if (value.includes('*')) if (value.includes('*'))
candidate = candidate.replace('*', matchedPartOfSpecifier); candidate = candidate.replace('*', matchedPartOfSpecifier);
candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate.replace(/\//g, path.sep)); candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate.replace(/\//g, path.sep));
for (const ext of ['', '.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx', '.cjs', '.mts', '.cts']) { if (isModule) {
if (fs.existsSync(candidate + ext)) { const transformed = js2ts(candidate);
if (transformed || fs.existsSync(candidate)) {
if (keyPrefix.length > longestPrefixLength) { if (keyPrefix.length > longestPrefixLength) {
longestPrefixLength = keyPrefix.length; longestPrefixLength = keyPrefix.length;
pathMatchedByLongestPrefix = candidate; pathMatchedByLongestPrefix = transformed || candidate;
}
}
} else {
for (const ext of ['', '.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx', '.cjs', '.mts', '.cts']) {
if (fs.existsSync(candidate + ext)) {
if (keyPrefix.length > longestPrefixLength) {
longestPrefixLength = keyPrefix.length;
pathMatchedByLongestPrefix = candidate;
}
} }
} }
} }
@ -145,13 +155,17 @@ export function resolveHook(filename: string, specifier: string): string | undef
if (pathMatchedByLongestPrefix) if (pathMatchedByLongestPrefix)
return pathMatchedByLongestPrefix; return pathMatchedByLongestPrefix;
} }
if (specifier.endsWith('.js')) {
const resolved = path.resolve(path.dirname(filename), specifier); if (isModule)
if (resolved.endsWith('.js')) { return js2ts(path.resolve(path.dirname(filename), specifier));
const tsResolved = resolved.substring(0, resolved.length - 3) + '.ts'; }
if (!fs.existsSync(resolved) && fs.existsSync(tsResolved))
return tsResolved; export function js2ts(resolved: string): string | undefined {
} const match = resolved.match(/(.*)(\.js|\.jsx|\.mjs)$/);
if (match) {
const tsResolved = match[1] + match[2].replace('j', 't');
if (!fs.existsSync(resolved) && fs.existsSync(tsResolved))
return tsResolved;
} }
} }
@ -197,7 +211,7 @@ export function installTransform(): () => void {
const originalResolveFilename = (Module as any)._resolveFilename; const originalResolveFilename = (Module as any)._resolveFilename;
function resolveFilename(this: any, specifier: string, parent: Module, ...rest: any[]) { function resolveFilename(this: any, specifier: string, parent: Module, ...rest: any[]) {
if (!reverted && parent) { if (!reverted && parent) {
const resolved = resolveHook(parent.filename, specifier); const resolved = resolveHook(false, parent.filename, specifier);
if (resolved !== undefined) if (resolved !== undefined)
specifier = resolved; specifier = resolved;
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-webkit", "name": "playwright-webkit",
"version": "1.27.0-next", "version": "1.27.1",
"description": "A high-level API to automate WebKit", "description": "A high-level API to automate WebKit",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -28,6 +28,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright", "name": "playwright",
"version": "1.27.0-next", "version": "1.27.1",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -28,6 +28,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.27.0-next" "playwright-core": "1.27.1"
} }
} }

View file

@ -610,6 +610,7 @@ export interface DebugControllerChannel extends DebugControllerEventTarget, Chan
} }
export type DebugControllerInspectRequestedEvent = { export type DebugControllerInspectRequestedEvent = {
selector: string, selector: string,
locators: NameValue[],
}; };
export type DebugControllerBrowsersChangedEvent = { export type DebugControllerBrowsersChangedEvent = {
browsers: { browsers: {

View file

@ -693,6 +693,9 @@ DebugController:
inspectRequested: inspectRequested:
parameters: parameters:
selector: string selector: string
locators:
type: array
items: NameValue
browsersChanged: browsersChanged:
parameters: parameters:

View file

@ -132,6 +132,7 @@ export const Recorder: React.FC<RecorderProps> = ({
<div>Target:</div> <div>Target:</div>
<select className='recorder-chooser' hidden={!sources.length} value={fileId} onChange={event => { <select className='recorder-chooser' hidden={!sources.length} value={fileId} onChange={event => {
setFileId(event.target.selectedOptions[0].value); setFileId(event.target.selectedOptions[0].value);
window.dispatch({ event: 'fileChanged', params: { file: event.target.selectedOptions[0].value } });
}}>{renderSourceOptions(sources)}</select> }}>{renderSourceOptions(sources)}</select>
<ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => { <ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => {
window.dispatch({ event: 'clear' }); window.dispatch({ event: 'clear' });

View file

@ -19,7 +19,7 @@ export type Point = { x: number, y: number };
export type Mode = 'inspecting' | 'recording' | 'none'; export type Mode = 'inspecting' | 'recording' | 'none';
export type EventData = { export type EventData = {
event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode' | 'selectorUpdated'; event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode' | 'selectorUpdated' | 'fileChanged';
params: any; params: any;
}; };
@ -27,6 +27,7 @@ export type UIState = {
mode: Mode; mode: Mode;
actionPoint?: Point; actionPoint?: Point;
actionSelector?: string; actionSelector?: string;
language: 'javascript' | 'python' | 'java' | 'csharp';
}; };
export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused'; export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused';

View file

@ -350,9 +350,11 @@ it('should be able to send third party cookies via an iframe', async ({ browser,
} }
}); });
it('should support requestStorageAccess', async ({ page, server, browserName, isMac, isLinux, isWindows }) => { it('should support requestStorageAccess', async ({ page, server, channel, browserName, isMac, isLinux, isWindows }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/17285' }); it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/17285' });
it.skip(browserName === 'chromium', 'requestStorageAccess API is not available in Chromium'); it.skip(browserName === 'chromium', 'requestStorageAccess API is not available in Chromium');
it.fixme(channel === 'firefox-beta', 'hasStorageAccess returns true, but no cookie is sent');
server.setRoute('/set-cookie.html', (req, res) => { server.setRoute('/set-cookie.html', (req, res) => {
res.setHeader('Set-Cookie', 'name=value; Path=/'); res.setHeader('Set-Cookie', 'name=value; Path=/');
res.end(); res.end();
@ -372,7 +374,6 @@ it('should support requestStorageAccess', async ({ page, server, browserName, is
]); ]);
expect(serverRequest.headers.cookie).toBe('name=value'); expect(serverRequest.headers.cookie).toBe('name=value');
} }
return;
} else { } else {
if (isLinux && browserName === 'webkit') if (isLinux && browserName === 'webkit')
expect(await frame.evaluate(() => document.hasStorageAccess())).toBeTruthy(); expect(await frame.evaluate(() => document.hasStorageAccess())).toBeTruthy();

View file

@ -34,20 +34,20 @@ test.describe('cli codegen', () => {
page.dispatchEvent('button', 'click', { detail: 1 }) page.dispatchEvent('button', 'click', { detail: 1 })
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect.soft(sources.get('JavaScript').text).toContain(`
await page.getByRole('button', { name: 'Submit' }).click();`); await page.getByRole('button', { name: 'Submit' }).click();`);
expect(sources.get('Python').text).toContain(` expect.soft(sources.get('Python').text).toContain(`
page.get_by_role("button", name="Submit").click()`); page.get_by_role("button", name="Submit").click()`);
expect(sources.get('Python Async').text).toContain(` expect.soft(sources.get('Python Async').text).toContain(`
await page.get_by_role("button", name="Submit").click()`); await page.get_by_role("button", name="Submit").click()`);
expect(sources.get('Java').text).toContain(` expect.soft(sources.get('Java').text).toContain(`
page.getByRole("button", new Page.GetByRoleOptions().setName("Submit")).click()`); page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit")).click()`);
expect(sources.get('C#').text).toContain(` expect.soft(sources.get('C#').text).toContain(`
await page.GetByRole("button", new () { Name = "Submit" }).ClickAsync();`); await page.GetByRole(AriaRole.Button, new() { NameString = "Submit" }).ClickAsync();`);
expect(message.text()).toBe('click'); expect(message.text()).toBe('click');
}); });
@ -157,20 +157,20 @@ test.describe('cli codegen', () => {
page.dispatchEvent('button', 'click', { detail: 1 }) page.dispatchEvent('button', 'click', { detail: 1 })
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect.soft(sources.get('JavaScript').text).toContain(`
await page.getByRole('button', { name: 'Submit' }).click();`); await page.getByRole('button', { name: 'Submit' }).click();`);
expect(sources.get('Python').text).toContain(` expect.soft(sources.get('Python').text).toContain(`
page.get_by_role("button", name="Submit").click()`); page.get_by_role("button", name="Submit").click()`);
expect(sources.get('Python Async').text).toContain(` expect.soft(sources.get('Python Async').text).toContain(`
await page.get_by_role("button", name="Submit").click()`); await page.get_by_role("button", name="Submit").click()`);
expect(sources.get('Java').text).toContain(` expect.soft(sources.get('Java').text).toContain(`
page.getByRole("button", new Page.GetByRoleOptions().setName("Submit")).click()`); page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit")).click()`);
expect(sources.get('C#').text).toContain(` expect.soft(sources.get('C#').text).toContain(`
await page.GetByRole("button", new () { Name = "Submit" }).ClickAsync();`); await page.GetByRole(AriaRole.Button, new() { NameString = "Submit" }).ClickAsync();`);
expect(message.text()).toBe('click'); expect(message.text()).toBe('click');
}); });
@ -548,31 +548,31 @@ test.describe('cli codegen', () => {
page.dispatchEvent('a', 'click', { detail: 1 }) page.dispatchEvent('a', 'click', { detail: 1 })
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect.soft(sources.get('JavaScript').text).toContain(`
const [page1] = await Promise.all([ const [page1] = await Promise.all([
page.waitForEvent('popup'), page.waitForEvent('popup'),
page.getByRole('link', { name: 'link' }).click() page.getByRole('link', { name: 'link' }).click()
]);`); ]);`);
expect(sources.get('Java').text).toContain(` expect.soft(sources.get('Java').text).toContain(`
Page page1 = page.waitForPopup(() -> { Page page1 = page.waitForPopup(() -> {
page.getByRole("link", new Page.GetByRoleOptions().setName("link")).click(); page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("link")).click();
});`); });`);
expect(sources.get('Python').text).toContain(` expect.soft(sources.get('Python').text).toContain(`
with page.expect_popup() as popup_info: with page.expect_popup() as popup_info:
page.get_by_role("link", name="link").click() page.get_by_role("link", name="link").click()
page1 = popup_info.value`); page1 = popup_info.value`);
expect(sources.get('Python Async').text).toContain(` expect.soft(sources.get('Python Async').text).toContain(`
async with page.expect_popup() as popup_info: async with page.expect_popup() as popup_info:
await page.get_by_role("link", name="link").click() await page.get_by_role("link", name="link").click()
page1 = await popup_info.value`); page1 = await popup_info.value`);
expect(sources.get('C#').text).toContain(` expect.soft(sources.get('C#').text).toContain(`
var page1 = await page.RunAndWaitForPopupAsync(async () => var page1 = await page.RunAndWaitForPopupAsync(async () =>
{ {
await page.GetByRole("link", new () { Name = "link" }).ClickAsync(); await page.GetByRole(AriaRole.Link, new() { NameString = "link" }).ClickAsync();
});`); });`);
expect(popup.url()).toBe('about:blank'); expect(popup.url()).toBe('about:blank');

View file

@ -226,41 +226,41 @@ test.describe('cli codegen', () => {
]); ]);
const sources = await recorder.waitForOutput('JavaScript', 'waitForEvent'); const sources = await recorder.waitForOutput('JavaScript', 'waitForEvent');
expect(sources.get('JavaScript').text).toContain(` expect.soft(sources.get('JavaScript').text).toContain(`
const context = await browser.newContext();`); const context = await browser.newContext();`);
expect(sources.get('JavaScript').text).toContain(` expect.soft(sources.get('JavaScript').text).toContain(`
const [download] = await Promise.all([ const [download] = await Promise.all([
page.waitForEvent('download'), page.waitForEvent('download'),
page.getByRole('link', { name: 'Download' }).click() page.getByRole('link', { name: 'Download' }).click()
]);`); ]);`);
expect(sources.get('Java').text).toContain(` expect.soft(sources.get('Java').text).toContain(`
BrowserContext context = browser.newContext();`); BrowserContext context = browser.newContext();`);
expect(sources.get('Java').text).toContain(` expect.soft(sources.get('Java').text).toContain(`
Download download = page.waitForDownload(() -> { Download download = page.waitForDownload(() -> {
page.getByRole("link", new Page.GetByRoleOptions().setName("Download")).click(); page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("Download")).click();
});`); });`);
expect(sources.get('Python').text).toContain(` expect.soft(sources.get('Python').text).toContain(`
context = browser.new_context()`); context = browser.new_context()`);
expect(sources.get('Python').text).toContain(` expect.soft(sources.get('Python').text).toContain(`
with page.expect_download() as download_info: with page.expect_download() as download_info:
page.get_by_role("link", name="Download").click() page.get_by_role("link", name="Download").click()
download = download_info.value`); download = download_info.value`);
expect(sources.get('Python Async').text).toContain(` expect.soft(sources.get('Python Async').text).toContain(`
context = await browser.new_context()`); context = await browser.new_context()`);
expect(sources.get('Python Async').text).toContain(` expect.soft(sources.get('Python Async').text).toContain(`
async with page.expect_download() as download_info: async with page.expect_download() as download_info:
await page.get_by_role("link", name="Download").click() await page.get_by_role("link", name="Download").click()
download = await download_info.value`); download = await download_info.value`);
expect(sources.get('C#').text).toContain(` expect.soft(sources.get('C#').text).toContain(`
var context = await browser.NewContextAsync();`); var context = await browser.NewContextAsync();`);
expect(sources.get('C#').text).toContain(` expect.soft(sources.get('C#').text).toContain(`
var download1 = await page.RunAndWaitForDownloadAsync(async () => var download1 = await page.RunAndWaitForDownloadAsync(async () =>
{ {
await page.GetByRole("link", new () { Name = "Download" }).ClickAsync(); await page.GetByRole(AriaRole.Link, new() { NameString = "Download" }).ClickAsync();
});`); });`);
}); });
@ -278,29 +278,29 @@ test.describe('cli codegen', () => {
const sources = await recorder.waitForOutput('JavaScript', 'once'); const sources = await recorder.waitForOutput('JavaScript', 'once');
expect(sources.get('JavaScript').text).toContain(` expect.soft(sources.get('JavaScript').text).toContain(`
page.once('dialog', dialog => { page.once('dialog', dialog => {
console.log(\`Dialog message: \${dialog.message()}\`); console.log(\`Dialog message: \${dialog.message()}\`);
dialog.dismiss().catch(() => {}); dialog.dismiss().catch(() => {});
}); });
await page.getByRole('button', { name: 'click me' }).click();`); await page.getByRole('button', { name: 'click me' }).click();`);
expect(sources.get('Java').text).toContain(` expect.soft(sources.get('Java').text).toContain(`
page.onceDialog(dialog -> { page.onceDialog(dialog -> {
System.out.println(String.format("Dialog message: %s", dialog.message())); System.out.println(String.format("Dialog message: %s", dialog.message()));
dialog.dismiss(); dialog.dismiss();
}); });
page.getByRole("button", new Page.GetByRoleOptions().setName("click me")).click();`); page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("click me")).click();`);
expect(sources.get('Python').text).toContain(` expect.soft(sources.get('Python').text).toContain(`
page.once(\"dialog\", lambda dialog: dialog.dismiss()) page.once(\"dialog\", lambda dialog: dialog.dismiss())
page.get_by_role("button", name="click me").click()`); page.get_by_role("button", name="click me").click()`);
expect(sources.get('Python Async').text).toContain(` expect.soft(sources.get('Python Async').text).toContain(`
page.once(\"dialog\", lambda dialog: dialog.dismiss()) page.once(\"dialog\", lambda dialog: dialog.dismiss())
await page.get_by_role("button", name="click me").click()`); await page.get_by_role("button", name="click me").click()`);
expect(sources.get('C#').text).toContain(` expect.soft(sources.get('C#').text).toContain(`
void page_Dialog1_EventHandler(object sender, IDialog dialog) void page_Dialog1_EventHandler(object sender, IDialog dialog)
{ {
Console.WriteLine($\"Dialog message: {dialog.Message}\"); Console.WriteLine($\"Dialog message: {dialog.Message}\");
@ -308,7 +308,7 @@ test.describe('cli codegen', () => {
page.Dialog -= page_Dialog1_EventHandler; page.Dialog -= page_Dialog1_EventHandler;
} }
page.Dialog += page_Dialog1_EventHandler; page.Dialog += page_Dialog1_EventHandler;
await page.GetByRole("button", new () { Name = "click me" }).ClickAsync();`); await page.GetByRole(AriaRole.Button, new() { NameString = "click me" }).ClickAsync();`);
}); });

View file

@ -46,10 +46,10 @@ test.describe('cli codegen', () => {
await page.get_by_role("button", name="Submit").first.click()`); await page.get_by_role("button", name="Submit").first.click()`);
expect.soft(sources.get('Java').text).toContain(` expect.soft(sources.get('Java').text).toContain(`
page.getByRole("button", new Page.GetByRoleOptions().setName("Submit")).first().click();`); page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit")).first().click();`);
expect.soft(sources.get('C#').text).toContain(` expect.soft(sources.get('C#').text).toContain(`
await page.GetByRole("button", new () { Name = "Submit" }).First.ClickAsync();`); await page.GetByRole(AriaRole.Button, new() { NameString = "Submit" }).First.ClickAsync();`);
expect(message.text()).toBe('click1'); expect(message.text()).toBe('click1');
}); });
@ -81,10 +81,10 @@ test.describe('cli codegen', () => {
await page.get_by_role("button", name="Submit").nth(1).click()`); await page.get_by_role("button", name="Submit").nth(1).click()`);
expect.soft(sources.get('Java').text).toContain(` expect.soft(sources.get('Java').text).toContain(`
page.getByRole("button", new Page.GetByRoleOptions().setName("Submit")).nth(1).click();`); page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit")).nth(1).click();`);
expect.soft(sources.get('C#').text).toContain(` expect.soft(sources.get('C#').text).toContain(`
await page.GetByRole("button", new () { Name = "Submit" }).Nth(1).ClickAsync();`); await page.GetByRole(AriaRole.Button, new() { NameString = "Submit" }).Nth(1).ClickAsync();`);
expect(message.text()).toBe('click2'); expect(message.text()).toBe('click2');
}); });
@ -217,7 +217,7 @@ test.describe('cli codegen', () => {
await page.frameLocator('#frame1').getByRole('button', { name: 'Submit' }).click();`); await page.frameLocator('#frame1').getByRole('button', { name: 'Submit' }).click();`);
expect.soft(sources.get('Java').text).toContain(` expect.soft(sources.get('Java').text).toContain(`
page.frameLocator("#frame1").getByRole("button", new FrameLocator.GetByRoleOptions().setName("Submit")).click();`); page.frameLocator("#frame1").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Submit")).click();`);
expect.soft(sources.get('Python').text).toContain(` expect.soft(sources.get('Python').text).toContain(`
page.frame_locator("#frame1").get_by_role("button", name="Submit").click()`); page.frame_locator("#frame1").get_by_role("button", name="Submit").click()`);
@ -226,7 +226,7 @@ test.describe('cli codegen', () => {
await page.frame_locator("#frame1").get_by_role("button", name="Submit").click()`); await page.frame_locator("#frame1").get_by_role("button", name="Submit").click()`);
expect.soft(sources.get('C#').text).toContain(` expect.soft(sources.get('C#').text).toContain(`
await page.FrameLocator("#frame1").GetByRole("button", new () { Name = "Submit" }).ClickAsync();`); await page.FrameLocator("#frame1").GetByRole(AriaRole.Button, new() { NameString = "Submit" }).ClickAsync();`);
}); });
test('should generate getByTestId', async ({ page, openRecorder }) => { test('should generate getByTestId', async ({ page, openRecorder }) => {
@ -267,7 +267,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input placeholder="Country"></input>`); await recorder.setContentAndWait(`<input placeholder="Country"></input>`);
const selector = await recorder.hoverOverElement('input'); const selector = await recorder.hoverOverElement('input');
expect(selector).toBe('internal:attr=[placeholder="Country"]'); expect(selector).toBe('internal:attr=[placeholder="Country"i]');
const [sources] = await Promise.all([ const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'click'), recorder.waitForOutput('JavaScript', 'click'),
@ -296,7 +296,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input alt="Country"></input>`); await recorder.setContentAndWait(`<input alt="Country"></input>`);
const selector = await recorder.hoverOverElement('input'); const selector = await recorder.hoverOverElement('input');
expect(selector).toBe('internal:attr=[alt="Country"]'); expect(selector).toBe('internal:attr=[alt="Country"i]');
const [sources] = await Promise.all([ const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'click'), recorder.waitForOutput('JavaScript', 'click'),

View file

@ -0,0 +1,138 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { contextTest as it, expect } from '../config/browserTest';
import { asLocator } from '../../packages/playwright-core/lib/server/isomorphic/locatorGenerators';
import type { Locator } from 'playwright-core';
function generate(locator: Locator) {
const result: any = {};
for (const lang of ['javascript', 'python', 'java', 'csharp'])
result[lang] = asLocator(lang, (locator as any)._selector, false);
return result;
}
it('reverse engineer locators', async ({ page }) => {
expect.soft(generate(page.getByTestId('Hello'))).toEqual({
javascript: "getByTestId('Hello')",
python: 'get_by_test_id("Hello")',
java: 'getByTestId("Hello")',
csharp: 'GetByTestId("Hello")'
});
expect.soft(generate(page.getByTestId('He"llo'))).toEqual({
javascript: 'getByTestId(\'He"llo\')',
python: 'get_by_test_id("He\\\"llo")',
java: 'getByTestId("He\\\"llo")',
csharp: 'GetByTestId("He\\\"llo")'
});
expect.soft(generate(page.getByText('Hello', { exact: true }))).toEqual({
csharp: 'GetByText("Hello", new() { Exact: true })',
java: 'getByText("Hello", new Page.GetByTextOptions().setExact(exact))',
javascript: 'getByText(\'Hello\', { exact: true })',
python: 'get_by_text("Hello", exact=true)',
});
expect.soft(generate(page.getByText('Hello'))).toEqual({
csharp: 'GetByText("Hello")',
java: 'getByText("Hello")',
javascript: 'getByText(\'Hello\')',
python: 'get_by_text("Hello")',
});
expect.soft(generate(page.getByText(/Hello/))).toEqual({
csharp: 'GetByText(new Regex("Hello"))',
java: 'getByText(Pattern.compile("Hello"))',
javascript: 'getByText(/Hello/)',
python: 'get_by_text(re.compile(r"Hello"))',
});
expect.soft(generate(page.getByLabel('Name'))).toEqual({
csharp: 'GetByLabel("Name")',
java: 'getByLabel("Name")',
javascript: 'getByLabel(\'Name\')',
python: 'get_by_label("Name")',
});
expect.soft(generate(page.getByLabel('Last Name', { exact: true }))).toEqual({
csharp: 'GetByLabel("Last Name", new() { Exact: true })',
java: 'getByLabel("Last Name", new Page.GetByLabelOptions().setExact(exact))',
javascript: 'getByLabel(\'Last Name\', { exact: true })',
python: 'get_by_label("Last Name", exact=true)',
});
expect.soft(generate(page.getByLabel(/Last\s+name/i))).toEqual({
csharp: 'GetByLabel(new Regex("Last\\\\s+name", RegexOptions.IgnoreCase))',
java: 'getByLabel(Pattern.compile("Last\\\\s+name", Pattern.CASE_INSENSITIVE))',
javascript: 'getByLabel(/Last\\s+name/i)',
python: 'get_by_label(re.compile(r"Last\\\\s+name", re.IGNORECASE))',
});
expect.soft(generate(page.getByPlaceholder('hello'))).toEqual({
csharp: 'GetByPlaceholder("hello")',
java: 'getByPlaceholder("hello")',
javascript: 'getByPlaceholder(\'hello\')',
python: 'get_by_placeholder("hello")',
});
expect.soft(generate(page.getByPlaceholder('Hello', { exact: true }))).toEqual({
csharp: 'GetByPlaceholder("Hello", new() { Exact: true })',
java: 'getByPlaceholder("Hello", new Page.GetByPlaceholderOptions().setExact(exact))',
javascript: 'getByPlaceholder(\'Hello\', { exact: true })',
python: 'get_by_placeholder("Hello", exact=true)',
});
expect.soft(generate(page.getByPlaceholder(/wor/i))).toEqual({
csharp: 'GetByPlaceholder(new Regex("wor", RegexOptions.IgnoreCase))',
java: 'getByPlaceholder(Pattern.compile("wor", Pattern.CASE_INSENSITIVE))',
javascript: 'getByPlaceholder(/wor/i)',
python: 'get_by_placeholder(re.compile(r"wor", re.IGNORECASE))',
});
expect.soft(generate(page.getByAltText('hello'))).toEqual({
csharp: 'GetByAltText("hello")',
java: 'getByAltText("hello")',
javascript: 'getByAltText(\'hello\')',
python: 'get_by_alt_text("hello")',
});
expect.soft(generate(page.getByAltText('Hello', { exact: true }))).toEqual({
csharp: 'GetByAltText("Hello", new() { Exact: true })',
java: 'getByAltText("Hello", new Page.GetByAltTextOptions().setExact(exact))',
javascript: 'getByAltText(\'Hello\', { exact: true })',
python: 'get_by_alt_text("Hello", exact=true)',
});
expect.soft(generate(page.getByAltText(/wor/i))).toEqual({
csharp: 'GetByAltText(new Regex("wor", RegexOptions.IgnoreCase))',
java: 'getByAltText(Pattern.compile("wor", Pattern.CASE_INSENSITIVE))',
javascript: 'getByAltText(/wor/i)',
python: 'get_by_alt_text(re.compile(r"wor", re.IGNORECASE))',
});
expect.soft(generate(page.getByTitle('hello'))).toEqual({
csharp: 'GetByTitle("hello")',
java: 'getByTitle("hello")',
javascript: 'getByTitle(\'hello\')',
python: 'get_by_title("hello")',
});
expect.soft(generate(page.getByTitle('Hello', { exact: true }))).toEqual({
csharp: 'GetByTitle("Hello", new() { Exact: true })',
java: 'getByTitle("Hello", new Page.GetByTitleOptions().setExact(exact))',
javascript: 'getByTitle(\'Hello\', { exact: true })',
python: 'get_by_title("Hello", exact=true)',
});
expect.soft(generate(page.getByTitle(/wor/i))).toEqual({
csharp: 'GetByTitle(new Regex("wor", RegexOptions.IgnoreCase))',
java: 'getByTitle(Pattern.compile("wor", Pattern.CASE_INSENSITIVE))',
javascript: 'getByTitle(/wor/i)',
python: 'get_by_title(re.compile(r"wor", re.IGNORECASE))',
});
});

View file

@ -45,7 +45,7 @@ it.describe('selector generator', () => {
it('should not escape spaces inside named attr selectors', async ({ page }) => { it('should not escape spaces inside named attr selectors', async ({ page }) => {
await page.setContent(`<input placeholder="Foo b ar"/>`); await page.setContent(`<input placeholder="Foo b ar"/>`);
expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"Foo b ar\"]'); expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"Foo b ar\"i]');
}); });
it('should generate text for <input type=button>', async ({ page }) => { it('should generate text for <input type=button>', async ({ page }) => {
@ -232,7 +232,7 @@ it.describe('selector generator', () => {
}); });
it('placeholder', async ({ page }) => { it('placeholder', async ({ page }) => {
await page.setContent(`<input placeholder="foobar" type="text"/>`); await page.setContent(`<input placeholder="foobar" type="text"/>`);
expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"foobar\"]'); expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"foobar\"i]');
}); });
it('type', async ({ page }) => { it('type', async ({ page }) => {
await page.setContent(`<input type="text"/>`); await page.setContent(`<input type="text"/>`);

View file

@ -27,7 +27,7 @@ it('should highlight locator', async ({ page, isAndroid }) => {
const textPromise = waitForTestLog<string>(page, 'Highlight text for test: '); const textPromise = waitForTestLog<string>(page, 'Highlight text for test: ');
const boxPromise = waitForTestLog<{ x: number, y: number, width: number, height: number }>(page, 'Highlight box for test: '); const boxPromise = waitForTestLog<{ x: number, y: number, width: number, height: number }>(page, 'Highlight box for test: ');
await page.locator('input').highlight(); await page.locator('input').highlight();
expect(await textPromise).toBe('input'); expect(await textPromise).toBe('locator(\'input\')');
let box1 = await page.locator('input').boundingBox(); let box1 = await page.locator('input').boundingBox();
let box2 = await boxPromise; let box2 = await boxPromise;

View file

@ -38,7 +38,7 @@ it('getByText should work', async ({ page }) => {
expect(await page.getByText('ye', { exact: true }).first().evaluate(e => e.outerHTML)).toContain('> ye </div>'); expect(await page.getByText('ye', { exact: true }).first().evaluate(e => e.outerHTML)).toContain('> ye </div>');
await page.setContent(`<div>Hello world</div><div>Hello</div>`); await page.setContent(`<div>Hello world</div><div>Hello</div>`);
expect(await page.getByText('Hello', { exact: true }).evaluate(e => e.outerHTML)).toBe('<div>Hello</div>'); expect(await page.getByText('Hello', { exact: true }).evaluate(e => e.outerHTML)).toContain('>Hello</div>');
}); });
it('getByLabel should work', async ({ page }) => { it('getByLabel should work', async ({ page }) => {
@ -108,29 +108,31 @@ it('getByTitle should work', async ({ page }) => {
}); });
it('getBy escaping', async ({ page }) => { it('getBy escaping', async ({ page }) => {
await page.setContent(`<label id=label for=control>Hello await page.setContent(`<label id=label for=control>Hello my
wo"rld</label><input id=control />`); wo"rld</label><input id=control />`);
await page.$eval('input', input => { await page.$eval('input', input => {
input.setAttribute('placeholder', 'hello\nwo"rld'); input.setAttribute('placeholder', 'hello my\nwo"rld');
input.setAttribute('title', 'hello\nwo"rld'); input.setAttribute('title', 'hello my\nwo"rld');
input.setAttribute('alt', 'hello\nwo"rld'); input.setAttribute('alt', 'hello my\nwo"rld');
}); });
await expect(page.getByText('hello\nwo"rld')).toHaveAttribute('id', 'label'); await expect(page.getByText('hello my\nwo"rld')).toHaveAttribute('id', 'label');
await expect(page.getByLabel('hello\nwo"rld')).toHaveAttribute('id', 'control'); await expect(page.getByText('hello my wo"rld')).toHaveAttribute('id', 'label');
await expect(page.getByPlaceholder('hello\nwo"rld')).toHaveAttribute('id', 'control'); await expect(page.getByLabel('hello my\nwo"rld')).toHaveAttribute('id', 'control');
await expect(page.getByAltText('hello\nwo"rld')).toHaveAttribute('id', 'control'); await expect(page.getByPlaceholder('hello my\nwo"rld')).toHaveAttribute('id', 'control');
await expect(page.getByTitle('hello\nwo"rld')).toHaveAttribute('id', 'control'); await expect(page.getByAltText('hello my\nwo"rld')).toHaveAttribute('id', 'control');
await expect(page.getByTitle('hello my\nwo"rld')).toHaveAttribute('id', 'control');
await page.setContent(`<label id=label for=control>Hello await page.setContent(`<label id=label for=control>Hello my
world</label><input id=control />`); world</label><input id=control />`);
await page.$eval('input', input => { await page.$eval('input', input => {
input.setAttribute('placeholder', 'hello\nworld'); input.setAttribute('placeholder', 'hello my\nworld');
input.setAttribute('title', 'hello\nworld'); input.setAttribute('title', 'hello my\nworld');
input.setAttribute('alt', 'hello\nworld'); input.setAttribute('alt', 'hello my\nworld');
}); });
await expect(page.getByText('hello\nworld')).toHaveAttribute('id', 'label'); await expect(page.getByText('hello my\nworld')).toHaveAttribute('id', 'label');
await expect(page.getByLabel('hello\nworld')).toHaveAttribute('id', 'control'); await expect(page.getByText('hello my world')).toHaveAttribute('id', 'label');
await expect(page.getByPlaceholder('hello\nworld')).toHaveAttribute('id', 'control'); await expect(page.getByLabel('hello my\nworld')).toHaveAttribute('id', 'control');
await expect(page.getByAltText('hello\nworld')).toHaveAttribute('id', 'control'); await expect(page.getByPlaceholder('hello my\nworld')).toHaveAttribute('id', 'control');
await expect(page.getByTitle('hello\nworld')).toHaveAttribute('id', 'control'); await expect(page.getByAltText('hello my\nworld')).toHaveAttribute('id', 'control');
await expect(page.getByTitle('hello my\nworld')).toHaveAttribute('id', 'control');
}); });

View file

@ -360,6 +360,10 @@ test('should support name', async ({ page }) => {
`<div role="button" aria-label="Hello"></div>`, `<div role="button" aria-label="Hello"></div>`,
`<div role="button" aria-label="Hello" aria-hidden="true"></div>`, `<div role="button" aria-label="Hello" aria-hidden="true"></div>`,
]); ]);
expect(await page.getByRole('button', { name: 'hello', includeHidden: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
`<div role="button" aria-label="Hello"></div>`,
`<div role="button" aria-label="Hello" aria-hidden="true"></div>`,
]);
expect(await page.locator(`role=button[name=Hello]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([ expect(await page.locator(`role=button[name=Hello]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
`<div role="button" aria-label="Hello"></div>`, `<div role="button" aria-label="Hello"></div>`,

View file

@ -109,7 +109,7 @@ test('should respect path resolver in experimental mode', async ({ runInlineTest
}, },
}`, }`,
'a.test.ts': ` 'a.test.ts': `
import { foo } from 'util/b.ts'; import { foo } from 'util/b.js';
const { test } = pwt; const { test } = pwt;
test('check project name', ({}, testInfo) => { test('check project name', ({}, testInfo) => {
expect(testInfo.project.name).toBe(foo); expect(testInfo.project.name).toBe(foo);

View file

@ -740,11 +740,11 @@ test('test.setTimeout should work separately in afterAll', async ({ runInlineTes
}); });
test.afterAll(async () => { test.afterAll(async () => {
console.log('\\n%%afterAll'); console.log('\\n%%afterAll');
test.setTimeout(1000); test.setTimeout(3000);
await new Promise(f => setTimeout(f, 800)); await new Promise(f => setTimeout(f, 2000));
}); });
`, `,
}, { timeout: '100' }); }, { timeout: '1000' });
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1); expect(result.passed).toBe(1);
expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([ expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([