Compare commits
23 commits
main
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f95d87228a | ||
|
|
63d6a35ce5 | ||
|
|
fde19c1652 | ||
|
|
04b3b7190c | ||
|
|
3367ebd968 | ||
|
|
730d3226a9 | ||
|
|
384b710c24 | ||
|
|
df7906d92a | ||
|
|
f87d6f2838 | ||
|
|
4ff2f3d0c5 | ||
|
|
75aaaac3eb | ||
|
|
af0d2936ac | ||
|
|
c05225f9b9 | ||
|
|
3f0af1e4df | ||
|
|
923a583174 | ||
|
|
b85b1ec6a0 | ||
|
|
299bceaada | ||
|
|
03c7f9c2f0 | ||
|
|
3bf42ce163 | ||
|
|
2993281d78 | ||
|
|
1bc426b94f | ||
|
|
cfa554972b | ||
|
|
1150de8e75 |
|
|
@ -9,22 +9,22 @@ await locator.click();
|
|||
```
|
||||
|
||||
```java
|
||||
Locator locator = page.frameLocator("#my-frame").locator("text=Submit");
|
||||
Locator locator = page.frameLocator("#my-frame").getByText("Submit");
|
||||
locator.click();
|
||||
```
|
||||
|
||||
```python async
|
||||
locator = page.frame_locator("#my-frame").locator("text=Submit")
|
||||
locator = page.frame_locator("#my-frame").get_by_text("Submit")
|
||||
await locator.click()
|
||||
```
|
||||
|
||||
```python sync
|
||||
locator = page.frame_locator("my-frame").locator("text=Submit")
|
||||
locator = page.frame_locator("my-frame").get_by_text("Submit")
|
||||
locator.click()
|
||||
```
|
||||
|
||||
```csharp
|
||||
var locator = page.FrameLocator("#my-frame").Locator("text=Submit");
|
||||
var locator = page.FrameLocator("#my-frame").GetByText("Submit");
|
||||
await locator.ClickAsync();
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1074,11 +1074,11 @@ Text to locate the element for.
|
|||
* since: v1.27
|
||||
- `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
|
||||
* 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.
|
||||
|
||||
|
|
@ -1178,7 +1178,109 @@ Locate element by the test id. By default, the `data-testid` attribute is used a
|
|||
|
||||
## 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
|
||||
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ steps:
|
|||
name: 'Playwright Tests'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.27.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.27.1-focal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v2
|
||||
|
|
@ -194,7 +194,7 @@ steps:
|
|||
name: 'Playwright Tests'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.27.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.27.1-focal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
|
|
@ -218,7 +218,7 @@ steps:
|
|||
name: 'Playwright Tests'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.27.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.27.1-focal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v3
|
||||
|
|
@ -239,7 +239,7 @@ steps:
|
|||
name: 'Playwright Tests'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.27.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.27.1-focal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup dotnet
|
||||
|
|
@ -264,7 +264,7 @@ steps:
|
|||
name: 'Playwright Tests - ${{ matrix.project }} - Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }}'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.27.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.27.1-focal
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
@ -299,7 +299,7 @@ jobs:
|
|||
- deployment: Run_E2E_Tests
|
||||
pool:
|
||||
vmImage: ubuntu-20.04
|
||||
container: mcr.microsoft.com/playwright:v1.27.0-focal
|
||||
container: mcr.microsoft.com/playwright:v1.27.1-focal
|
||||
environment: testing
|
||||
strategy:
|
||||
runOnce:
|
||||
|
|
@ -325,7 +325,7 @@ jobs:
|
|||
- deployment: Run_E2E_Tests
|
||||
pool:
|
||||
vmImage: ubuntu-20.04
|
||||
container: mcr.microsoft.com/playwright:v1.27.0-focal
|
||||
container: mcr.microsoft.com/playwright:v1.27.1-focal
|
||||
environment: testing
|
||||
strategy:
|
||||
runOnce:
|
||||
|
|
@ -368,7 +368,7 @@ Running Playwright on Circle CI is very similar to running on GitHub Actions. In
|
|||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.27.0-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.27.1-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed if playwright is in `devDependencies`
|
||||
```
|
||||
|
|
@ -404,7 +404,7 @@ to run tests on Jenkins.
|
|||
|
||||
```groovy
|
||||
pipeline {
|
||||
agent { docker { image 'mcr.microsoft.com/playwright:v1.27.0-focal' } }
|
||||
agent { docker { image 'mcr.microsoft.com/playwright:v1.27.1-focal' } }
|
||||
stages {
|
||||
stage('e2e-tests') {
|
||||
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)).
|
||||
|
||||
```yml
|
||||
image: mcr.microsoft.com/playwright:v1.27.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.27.1-focal
|
||||
```
|
||||
|
||||
### GitLab CI
|
||||
|
|
@ -435,7 +435,7 @@ stages:
|
|||
|
||||
tests:
|
||||
stage: test
|
||||
image: mcr.microsoft.com/playwright:v1.27.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.27.1-focal
|
||||
script:
|
||||
...
|
||||
```
|
||||
|
|
@ -451,7 +451,7 @@ stages:
|
|||
|
||||
tests:
|
||||
stage: test
|
||||
image: mcr.microsoft.com/playwright:v1.27.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.27.1-focal
|
||||
parallel: 7
|
||||
script:
|
||||
- npm ci
|
||||
|
|
@ -466,7 +466,7 @@ stages:
|
|||
|
||||
tests:
|
||||
stage: test
|
||||
image: mcr.microsoft.com/playwright:v1.27.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.27.1-focal
|
||||
parallel:
|
||||
matrix:
|
||||
- PROJECT: ['chromium', 'webkit']
|
||||
|
|
|
|||
|
|
@ -13,37 +13,34 @@ Playwright comes with the ability to generate tests out of the box and is a grea
|
|||
## Running Codegen
|
||||
|
||||
```bash js
|
||||
npx playwright codegen playwright.dev
|
||||
npx playwright codegen demo.playwright.dev/todomvc
|
||||
```
|
||||
|
||||
```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
|
||||
playwright codegen playwright.dev
|
||||
playwright codegen demo.playwright.dev/todomvc
|
||||
```
|
||||
|
||||
```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.
|
||||
|
||||
<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>
|
||||
|
||||
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.
|
||||
|
||||
<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" />
|
||||
|
||||
|
||||
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).
|
||||
|
||||
|
||||
## What's Next
|
||||
|
||||
- [See a trace of your tests](./trace-viewer-intro.md)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
||||
## Running Codegen
|
||||
|
||||
```bash js
|
||||
npx playwright codegen playwright.dev
|
||||
npx playwright codegen demo.playwright.dev/todomvc
|
||||
```
|
||||
|
||||
```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
|
||||
playwright codegen playwright.dev
|
||||
playwright codegen demo.playwright.dev/todomvc
|
||||
```
|
||||
|
||||
```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.
|
||||
|
||||
<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
|
||||
|
||||
|
|
@ -72,8 +75,7 @@ playwright 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
|
||||
|
||||
|
|
|
|||
|
|
@ -14,19 +14,19 @@ This image is published on [Docker Hub].
|
|||
### Pull the image
|
||||
|
||||
```bash js
|
||||
docker pull mcr.microsoft.com/playwright:v1.27.0-focal
|
||||
docker pull mcr.microsoft.com/playwright:v1.27.1-focal
|
||||
```
|
||||
|
||||
```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
|
||||
docker pull mcr.microsoft.com/playwright/dotnet:v1.27.0-focal
|
||||
docker pull mcr.microsoft.com/playwright/dotnet:v1.27.1-focal
|
||||
```
|
||||
|
||||
```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
|
||||
|
|
@ -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.
|
||||
|
||||
```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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
|
@ -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.
|
||||
|
||||
```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
|
||||
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
|
||||
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
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
<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:
|
||||
|
||||
|
|
@ -19,39 +19,42 @@ Once installed, open the command panel and type:
|
|||
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).
|
||||
|
||||
<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
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
<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 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
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
<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
|
||||
|
||||
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
|
||||
|
|
@ -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.
|
||||
|
||||
|
||||
<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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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" />
|
||||
<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" />
|
||||
|
||||
### 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.
|
||||
|
||||
<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" />
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -253,3 +253,79 @@ unless page navigates or the handle is manually disposed via the [`method: JSHan
|
|||
- [`method: Page.evaluateHandle`]
|
||||
- [`method: Page.querySelector`]
|
||||
- [`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();
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
const locator = page.getByText('Submit');
|
||||
await locator.click();
|
||||
await page.getByLabel('User Name').fill('John');
|
||||
|
||||
await page.getByLabel('Password').fill('secret-password');
|
||||
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
|
||||
await expect(page.getByText('Welcome, John!')).toBeVisible();
|
||||
```
|
||||
|
||||
```java
|
||||
Locator locator = page.getByText("Submit");
|
||||
locator.click();
|
||||
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();
|
||||
```
|
||||
|
||||
```python async
|
||||
locator = page.get_by_text("Submit")
|
||||
await locator.click()
|
||||
await page.get_by_label("User Name").fill("John")
|
||||
|
||||
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
|
||||
locator = page.get_by_text("Submit")
|
||||
locator.click()
|
||||
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()
|
||||
```
|
||||
|
||||
```csharp
|
||||
var locator = page.GetByText("Submit");
|
||||
await locator.ClickAsync();
|
||||
await page.GetByLabel("User Name").FillAsync("John");
|
||||
|
||||
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
|
||||
|
|
@ -71,75 +108,57 @@ await locator.ClickAsync();
|
|||
|
||||
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
|
||||
given selector.
|
||||
given selector. For example, the following call throws if there are several buttons in the DOM:
|
||||
|
||||
```js
|
||||
// Throws if there are several buttons in DOM:
|
||||
await page.getByRole('button').click();
|
||||
```
|
||||
|
||||
// Works because we explicitly tell locator to pick the first element:
|
||||
await page.getByRole('button').first().click(); // ⚠️ using first disables strictness
|
||||
```python async
|
||||
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();
|
||||
```
|
||||
|
||||
```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()
|
||||
```
|
||||
|
||||
```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()
|
||||
```
|
||||
|
||||
```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();
|
||||
```
|
||||
|
||||
```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();
|
||||
```
|
||||
|
||||
:::caution
|
||||
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.
|
||||
:::
|
||||
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.
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
### Locate by text content using `text=`
|
||||
|
||||
The easiest way to find an element is to look for the text it contains.
|
||||
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.
|
||||
|
||||
```js
|
||||
await page.getByText('Log in').click();
|
||||
|
|
@ -157,29 +176,38 @@ page.get_by_text("Log in").click()
|
|||
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
|
||||
await page.locator('data-test-id=product-item', { hasText: 'Playwright Book' }).click();
|
||||
const locator = page.frameLocator('#my-frame').getByText('Submit');
|
||||
await locator.click();
|
||||
```
|
||||
|
||||
```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
|
||||
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
|
||||
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
|
||||
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 `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.
|
||||
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.
|
||||
|
||||
```js
|
||||
await page.getByRole('button', { name: /submit/i }).click();
|
||||
|
|
@ -188,113 +216,255 @@ await page.getByRole('checkbox', { checked: true, name: "Check me" }).check();
|
|||
```
|
||||
|
||||
```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
|
||||
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
|
||||
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();
|
||||
```
|
||||
|
||||
```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();
|
||||
```
|
||||
|
||||
[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=`
|
||||
|
||||
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();
|
||||
```
|
||||
Note that role locators **do not replace** accessibility audits and conformance tests, but rather give early feedback about the ARIA guidelines.
|
||||
|
||||
### 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.
|
||||
|
||||
```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:
|
||||
- `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:
|
||||
You can fill the input after locating it by the label text:
|
||||
|
||||
```js
|
||||
await page.getByText('Password').fill('secret');
|
||||
await page.getByLabel('Password').fill('secret');
|
||||
```
|
||||
|
||||
```java
|
||||
page.getByText("Password").fill("secret");
|
||||
page.getByLabel("Password").fill("secret");
|
||||
```
|
||||
|
||||
```python async
|
||||
await page.get_by_text('Password').fill('secret')
|
||||
await page.get_by_label("Password").fill("secret")
|
||||
```
|
||||
|
||||
```python sync
|
||||
page.get_by_text('Password').fill('secret')
|
||||
page.get_by_label("Password").fill("secret")
|
||||
```
|
||||
|
||||
```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
|
||||
|
||||
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:
|
||||
|
||||
```html
|
||||
<div data-test-id='product-card'>
|
||||
<div data-testid='product-card'>
|
||||
<span>Product 1</span>
|
||||
<button>Buy</button>
|
||||
</div>
|
||||
<div data-test-id='product-card'>
|
||||
<div data-testid='product-card'>
|
||||
<span>Product 2</span>
|
||||
<button>Buy</button>
|
||||
</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.
|
||||
|
||||
```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();
|
||||
```
|
||||
|
||||
```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()
|
||||
```
|
||||
|
||||
```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()
|
||||
```
|
||||
|
||||
```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();
|
||||
```
|
||||
|
||||
```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();
|
||||
```
|
||||
|
||||
### 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
|
||||
await page.locator('css=button').click();
|
||||
|
|
@ -376,11 +546,7 @@ 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. 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:
|
||||
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:
|
||||
|
||||
```js
|
||||
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();
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
```js
|
||||
await page.locator('button', { hasText: 'Click me' }).click();
|
||||
await page.locator('button', { hasText: /Click me/ }).click();
|
||||
await page.getByTestId('product-card').filter({ hasText: 'Product 3' }).click();
|
||||
await page.getByTestId('product-card').filter({ hasText: /product 3/ }).click();
|
||||
```
|
||||
```java
|
||||
page.locator("button", new Page.LocatorOptions().setHasText("Click me")).click();
|
||||
page.locator("button", new Page.LocatorOptions().setHasText(Pattern.compile("Click me"))).click();
|
||||
page.getByTestId("product-card").filter(new Locator.FilterOptions().setHasText("Product 3")).click();
|
||||
page.getByTestId("product-card").filter(new Locator.FilterOptions().setHasText(Pattern.compile("Product 3"))).click();
|
||||
```
|
||||
```python async
|
||||
await page.locator("button", has_text="Click me").click()
|
||||
await page.locator("button", has_text=re.compile("Click me")).click()
|
||||
await page.get_by_test_id("product-card").filter(has_text="Product 3").click()
|
||||
await page.get_by_test_id("product-card").filter(has_text=re.compile("Product 3")).click()
|
||||
```
|
||||
```python sync
|
||||
page.locator("button", has_text="Click me").click()
|
||||
page.locator("button", has_text=re.compile("Click me")).click()
|
||||
page.get_by_test_id("product-card").filter(has_text="Product 3").click()
|
||||
page.get_by_test_id("product-card").filter(has_text=re.compile("Product 3")).click()
|
||||
```
|
||||
```csharp
|
||||
await page.Locator("button", new() { HasText = "Click me" }).ClickAsync();
|
||||
await page.Locator("button", new() { HasText = new Regex("Click me") }).ClickAsync();
|
||||
await page.GetByTestId("product-card").Filter(new() { HasText = "Product 3" }).ClickAsync();
|
||||
await page.GetByTestId("product-card").Filter(new() { HasText = new Regex("Product 3") }).ClickAsync();
|
||||
```
|
||||
|
||||
#### 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.
|
||||
|
||||
```js
|
||||
page.locator('article', { has: page.locator('button.subscribe') })
|
||||
page.getByRole('section').filter({ has: page.getByTestId('subscribe-button') })
|
||||
```
|
||||
```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
|
||||
page.locator("article", has=page.locator("button.subscribe"))
|
||||
page.get_by_role("section").filter(has=page.get_by_test_id("subscribe-button"))
|
||||
```
|
||||
```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
|
||||
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.
|
||||
|
|
@ -472,7 +638,7 @@ const rowLocator = page.locator('tr');
|
|||
// ...
|
||||
await rowLocator
|
||||
.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();
|
||||
```
|
||||
```java
|
||||
|
|
@ -481,7 +647,7 @@ Locator rowLocator = page.locator("tr");
|
|||
rowLocator
|
||||
.filter(new Locator.FilterOptions().setHasText("text in column 1"))
|
||||
.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();
|
||||
```
|
||||
|
|
@ -490,7 +656,7 @@ row_locator = page.locator("tr")
|
|||
# ...
|
||||
await row_locator
|
||||
.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()
|
||||
```
|
||||
```python sync
|
||||
|
|
@ -498,7 +664,7 @@ row_locator = page.locator("tr")
|
|||
# ...
|
||||
row_locator
|
||||
.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()
|
||||
```
|
||||
```csharp
|
||||
|
|
@ -507,7 +673,7 @@ var rowLocator = page.Locator("tr");
|
|||
await rowLocator
|
||||
.Filter(new LocatorFilterOptions { HasText = "text in column 1" })
|
||||
.Filter(new LocatorFilterOptions {
|
||||
Has = page.Locator("tr", new PageLocatorOptions { HasText = "column 2 button" } )
|
||||
Has = page.GetByRole("button", new() { Name = "column 2 button" } )
|
||||
})
|
||||
.ScreenshotAsync();
|
||||
```
|
||||
|
|
@ -515,21 +681,21 @@ await rowLocator
|
|||
### Locate elements in Shadow DOM
|
||||
|
||||
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.
|
||||
|
||||
Consider the following example with a custom web component:
|
||||
```html
|
||||
<x-badge>
|
||||
<span>Title</span>
|
||||
<x-details role=button aria-expanded=true aria-controls=inner-details>
|
||||
<div>Title</div>
|
||||
#shadow-root
|
||||
<span>Details</span>
|
||||
</x-badge>
|
||||
<div id=inner-details>Details</div>
|
||||
</x-details>
|
||||
```
|
||||
|
||||
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
|
||||
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();
|
||||
```
|
||||
|
||||
- Click `<x-badge>`
|
||||
- Click `<x-details>`
|
||||
```js
|
||||
await page.locator('x-badge', { hasText: 'Details' }).click();
|
||||
await page.locator('x-details', { hasText: 'Details' }).click();
|
||||
```
|
||||
```java
|
||||
page.locator("x-badge", new Page.LocatorOptions().setHasText("Details")).click();
|
||||
page.locator("x-details", new Page.LocatorOptions().setHasText("Details")).click();
|
||||
```
|
||||
```python async
|
||||
await page.locator("x-badge", has_text="Details" ).click()
|
||||
await page.locator("x-details", has_text="Details" ).click()
|
||||
```
|
||||
```python sync
|
||||
page.locator("x-badge", has_text="Details" ).click()
|
||||
page.locator("x-details", has_text="Details" ).click()
|
||||
```
|
||||
```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
|
||||
await expect(page.locator('x-badge')).toContainText('Details');
|
||||
await expect(page.locator('x-details')).toContainText('Details');
|
||||
```
|
||||
```java
|
||||
assertThat(page.locator("x-badge")).containsText("Details");
|
||||
assertThat(page.locator("x-details")).containsText("Details");
|
||||
```
|
||||
```python async
|
||||
await expect(page.locator("x-badge")).to_contain_text("Details")
|
||||
await expect(page.locator("x-details")).to_contain_text("Details")
|
||||
```
|
||||
```python sync
|
||||
expect(page.locator("x-badge")).to_contain_text("Details")
|
||||
expect(page.locator("x-details")).to_contain_text("Details")
|
||||
```
|
||||
```csharp
|
||||
await Expect(page.Locator("x-badge")).ToContainTextAsync("Details");
|
||||
await Expect(page.Locator("x-details")).ToContainTextAsync("Details");
|
||||
```
|
||||
|
||||
## Lists
|
||||
|
|
@ -586,7 +752,7 @@ You can also use locators to work with the element lists.
|
|||
|
||||
```js
|
||||
// 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.
|
||||
const texts = await rows.allTextContents();
|
||||
|
|
@ -603,7 +769,7 @@ const texts = await rows.evaluateAll(list => list.map(element => element.textCon
|
|||
|
||||
```python async
|
||||
# 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.
|
||||
texts = await rows.all_text_contents()
|
||||
|
|
@ -620,7 +786,7 @@ texts = await rows.evaluate_all("list => list.map(element => element.textContent
|
|||
|
||||
```python sync
|
||||
# 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.
|
||||
texts = rows.all_text_contents()
|
||||
|
|
@ -637,7 +803,7 @@ texts = rows.evaluate_all("list => list.map(element => element.textContent)")
|
|||
|
||||
```java
|
||||
// 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.
|
||||
List<String> texts = rows.allTextContents();
|
||||
|
|
@ -654,7 +820,7 @@ Object texts = rows.evaluateAll("list => list.map(element => element.textContent
|
|||
|
||||
```csharp
|
||||
// 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.
|
||||
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`].
|
||||
|
||||
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:
|
||||
|
||||
```js
|
||||
await page.locator('data-test-id=product-card').nth(3).click();
|
||||
await page.getByTestId('product-card').nth(3).click();
|
||||
```
|
||||
|
||||
```java
|
||||
page.locator("data-test-id=product-card").nth(3).click();
|
||||
page.getByTestId("product-card").nth(3).click();
|
||||
```
|
||||
|
||||
```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
|
||||
page.locator("data-test-id=product-card").nth(3).click()
|
||||
page.get_by_test_id("product-card").nth(3).click()
|
||||
```
|
||||
|
||||
```csharp
|
||||
await page.Locator("data-test-id=product-card").Nth(3).ClickAsync();
|
||||
await page.GetByTestId("product-card").Nth(3).ClickAsync();
|
||||
```
|
||||
|
||||
## 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();
|
||||
```
|
||||
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).
|
||||
|
|
|
|||
|
|
@ -4,6 +4,55 @@ title: "Release notes"
|
|||
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
|
||||
|
||||
### Assertions
|
||||
|
|
|
|||
|
|
@ -4,6 +4,55 @@ title: "Release notes"
|
|||
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
|
||||
|
||||
### Assertions
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ 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.
|
||||
|
|
@ -143,7 +144,7 @@ This version was also tested against the following stable channels:
|
|||
|
||||
### 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 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.
|
||||
|
|
@ -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 ⚠️
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,55 @@ title: "Release notes"
|
|||
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
|
||||
|
||||
### Assertions
|
||||
|
|
@ -48,7 +97,7 @@ This version was also tested against the following stable channels:
|
|||
|
||||
### 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).
|
||||
* ⚠️ Ubuntu 18 is now deprecated and will not be supported as of Dec 2022.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
<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).
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ await page.Locator("text=Log in").ClickAsync();
|
|||
|
||||
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
|
||||
await page.locator('text=Log in').click();
|
||||
|
|
@ -254,7 +254,7 @@ Text selector has a few variations:
|
|||
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"`.
|
||||
|
||||
|
|
@ -310,7 +310,7 @@ Text selector has a few variations:
|
|||
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>`.
|
||||
```js
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
```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
|
||||
npx playwright test --update-snapshots
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,15 +3,133 @@ id: 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
|
||||
|
||||
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
|
||||
// @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.
|
||||
|
||||
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
|
||||
* langs: java, csharp, python
|
||||
|
|
@ -156,56 +274,16 @@ playwright 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:
|
||||
|
||||
<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.
|
||||
[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.
|
||||
|
||||
|
||||
## 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.
|
||||
They could be generated in a CI run and makes it easy to view the remote trace without having to manually download the file.
|
||||
## Viewing remote traces
|
||||
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
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
|
||||
groupId="test-runners"
|
||||
|
|
@ -36,7 +36,7 @@ public class Tests : PageTest
|
|||
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
|
||||
|
||||
// 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.
|
||||
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro");
|
||||
|
|
@ -71,7 +71,7 @@ public class UnitTest1 : PageTest
|
|||
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
|
||||
|
||||
// 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.
|
||||
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro");
|
||||
|
|
@ -99,22 +99,15 @@ await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
|
|||
|
||||
### 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
|
||||
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 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
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ Playwright assertions are created specifically for the dynamic web. Checks are a
|
|||
|
||||
## 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
|
||||
// @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/);
|
||||
|
||||
// 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.
|
||||
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/);
|
||||
|
||||
// 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.
|
||||
await expect(getStarted).toHaveAttribute('href', '/docs/intro');
|
||||
|
|
@ -79,23 +79,15 @@ await expect(page).toHaveTitle(/Playwright/);
|
|||
|
||||
### 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
|
||||
const getStarted = page.getByText('Get Started');
|
||||
const getStarted = page.getByRole('link', { name: 'Get started' });
|
||||
|
||||
await expect(getStarted).toHaveAttribute('href', '/docs/installation');
|
||||
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
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
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
|
||||
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"))
|
||||
|
||||
# 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(get_started).to_have_attribute("href", "/docs/intro")
|
||||
|
|
@ -46,27 +46,17 @@ expect(page).to_have_title(re.compile("Playwright"))
|
|||
|
||||
### 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
|
||||
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")
|
||||
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
|
||||
|
||||
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
66
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "playwright-internal",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
|
@ -6393,11 +6393,11 @@
|
|||
"version": "0.0.0"
|
||||
},
|
||||
"packages/playwright": {
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -6407,11 +6407,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-chromium": {
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -6421,7 +6421,7 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-core": {
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -6432,10 +6432,10 @@
|
|||
},
|
||||
"packages/playwright-ct-react": {
|
||||
"name": "@playwright/experimental-ct-react",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.27.0-next",
|
||||
"@playwright/test": "1.27.1",
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
"vite": "^3.0.9"
|
||||
},
|
||||
|
|
@ -6888,10 +6888,10 @@
|
|||
},
|
||||
"packages/playwright-ct-solid": {
|
||||
"name": "@playwright/experimental-ct-solid",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.27.0-next",
|
||||
"@playwright/test": "1.27.1",
|
||||
"vite": "^3.0.0",
|
||||
"vite-plugin-solid": "^2.3.0"
|
||||
},
|
||||
|
|
@ -7308,10 +7308,10 @@
|
|||
},
|
||||
"packages/playwright-ct-svelte": {
|
||||
"name": "@playwright/experimental-ct-svelte",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.27.0-next",
|
||||
"@playwright/test": "1.27.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||
"vite": "^3.0.0"
|
||||
},
|
||||
|
|
@ -7738,10 +7738,10 @@
|
|||
},
|
||||
"packages/playwright-ct-vue": {
|
||||
"name": "@playwright/experimental-ct-vue",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.27.0-next",
|
||||
"@playwright/test": "1.27.1",
|
||||
"@vitejs/plugin-vue": "^2.3.1",
|
||||
"vite": "^2.9.5"
|
||||
},
|
||||
|
|
@ -7786,10 +7786,10 @@
|
|||
},
|
||||
"packages/playwright-ct-vue2": {
|
||||
"name": "@playwright/experimental-ct-vue2",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.27.0-next",
|
||||
"@playwright/test": "1.27.1",
|
||||
"vite": "^2.9.5",
|
||||
"vite-plugin-vue2": "^2.0.1"
|
||||
},
|
||||
|
|
@ -7801,11 +7801,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-firefox": {
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7816,11 +7816,11 @@
|
|||
},
|
||||
"packages/playwright-test": {
|
||||
"name": "@playwright/test",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7830,11 +7830,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-webkit": {
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -8525,7 +8525,7 @@
|
|||
"@playwright/experimental-ct-react": {
|
||||
"version": "file:packages/playwright-ct-react",
|
||||
"requires": {
|
||||
"@playwright/test": "1.27.0-next",
|
||||
"@playwright/test": "1.27.1",
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
"vite": "^3.0.9"
|
||||
},
|
||||
|
|
@ -8734,7 +8734,7 @@
|
|||
"@playwright/experimental-ct-solid": {
|
||||
"version": "file:packages/playwright-ct-solid",
|
||||
"requires": {
|
||||
"@playwright/test": "1.27.0-next",
|
||||
"@playwright/test": "1.27.1",
|
||||
"solid-js": "^1.4.7",
|
||||
"vite": "^3.0.0",
|
||||
"vite-plugin-solid": "^2.3.0"
|
||||
|
|
@ -8919,7 +8919,7 @@
|
|||
"@playwright/experimental-ct-svelte": {
|
||||
"version": "file:packages/playwright-ct-svelte",
|
||||
"requires": {
|
||||
"@playwright/test": "1.27.0-next",
|
||||
"@playwright/test": "1.27.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||
"svelte": "^3.49.0",
|
||||
"vite": "^3.0.0"
|
||||
|
|
@ -9105,7 +9105,7 @@
|
|||
"@playwright/experimental-ct-vue": {
|
||||
"version": "file:packages/playwright-ct-vue",
|
||||
"requires": {
|
||||
"@playwright/test": "1.27.0-next",
|
||||
"@playwright/test": "1.27.1",
|
||||
"@vitejs/plugin-vue": "^2.3.1",
|
||||
"vite": "^2.9.5"
|
||||
},
|
||||
|
|
@ -9138,7 +9138,7 @@
|
|||
"@playwright/experimental-ct-vue2": {
|
||||
"version": "file:packages/playwright-ct-vue2",
|
||||
"requires": {
|
||||
"@playwright/test": "1.27.0-next",
|
||||
"@playwright/test": "1.27.1",
|
||||
"vite": "^2.9.5",
|
||||
"vite-plugin-vue2": "^2.0.1",
|
||||
"vue": "^2.6.14"
|
||||
|
|
@ -9148,7 +9148,7 @@
|
|||
"version": "file:packages/playwright-test",
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
}
|
||||
},
|
||||
"@rollup/pluginutils": {
|
||||
|
|
@ -11374,13 +11374,13 @@
|
|||
"playwright": {
|
||||
"version": "file:packages/playwright",
|
||||
"requires": {
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
}
|
||||
},
|
||||
"playwright-chromium": {
|
||||
"version": "file:packages/playwright-chromium",
|
||||
"requires": {
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
|
|
@ -11389,13 +11389,13 @@
|
|||
"playwright-firefox": {
|
||||
"version": "file:packages/playwright-firefox",
|
||||
"requires": {
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
}
|
||||
},
|
||||
"playwright-webkit": {
|
||||
"version": "file:packages/playwright-webkit",
|
||||
"requires": {
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"private": true,
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"license": "Apache-2.0",
|
||||
"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",
|
||||
"ftest": "playwright test --config=tests/library/playwright.config.ts --project=firefox",
|
||||
"wtest": "playwright test --config=tests/library/playwright.config.ts --project=webkit",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-chromium",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"description": "A high-level API to automate Chromium",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,6 +28,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-core",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
|
|||
|
|
@ -329,11 +329,11 @@ if (!process.env.PW_LANG_NAME) {
|
|||
require('playwright');
|
||||
hasPlaywrightPackage = true;
|
||||
} catch {}
|
||||
const strayPackage = hasPlaywrightPackage ? 'playwright' : 'playwright-core';
|
||||
console.error(wrapInASCIIBox([
|
||||
`Playwright Test compatibility check failed:`,
|
||||
`@playwright/test version '${pwTestVersion}' does not match ${hasPlaywrightPackage ? 'playwright' : 'playwright-core'} version '${pwCoreVersion}'!`,
|
||||
`To fix this either align the versions or only keep @playwright/test since it depends on playwright-core.`,
|
||||
`If you still receive this error, execute 'npm ci' or delete 'node_modules' and do 'npm install' again.`,
|
||||
`Playwright Test integrity check failed:`,
|
||||
`You have @playwright/test version '${pwTestVersion}' and '${strayPackage}' version '${pwCoreVersion}' installed!`,
|
||||
`You probably added '${strayPackage}' into your package.json by accident, remove it and re-run 'npm install'`,
|
||||
].join('\n'), 1));
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ class ProtocolHandler {
|
|||
this._controller.on(DebugController.Events.BrowsersChanged, browsers => {
|
||||
process.send!({ method: 'browsersChanged', params: { browsers } });
|
||||
});
|
||||
this._controller.on(DebugController.Events.InspectRequested, selector => {
|
||||
process.send!({ method: 'inspectRequested', params: { selector } });
|
||||
this._controller.on(DebugController.Events.InspectRequested, ({ selector, locators }) => {
|
||||
process.send!({ method: 'inspectRequested', params: { selector, locators } });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -396,7 +396,7 @@ export function setTestIdAttribute(attributeName: string) {
|
|||
function getByAttributeTextSelector(attrName: string, text: string | RegExp, options?: { exact?: boolean }): string {
|
||||
if (!isString(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 {
|
||||
|
|
@ -439,7 +439,7 @@ export function getByRoleSelector(role: string, options: ByRoleOptions = {}): st
|
|||
if (options.level !== undefined)
|
||||
props.push(['level', String(options.level)]);
|
||||
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)
|
||||
props.push(['pressed', String(options.pressed)]);
|
||||
return `role=${role}${props.map(([n, v]) => `[${n}=${v}]`).join('')}`;
|
||||
|
|
|
|||
|
|
@ -75,12 +75,12 @@ async function deletePlaywrightImage() {
|
|||
async function buildPlaywrightImage() {
|
||||
await checkDockerEngineIsRunningOrDie();
|
||||
|
||||
const isDevelopmentMode = getPlaywrightVersion().includes('next');
|
||||
let baseImageName = `mcr.microsoft.com/playwright:v${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`;
|
||||
// 1. Build or pull base image.
|
||||
let baseImageName = process.env.PWTEST_DOCKER_BASE_IMAGE || '';
|
||||
if (!baseImageName) {
|
||||
const isDevelopmentMode = getPlaywrightVersion().includes('next');
|
||||
if (isDevelopmentMode) {
|
||||
// Use our docker build scripts in development mode!
|
||||
if (!process.env.PWTEST_DOCKER_BASE_IMAGE) {
|
||||
const arch = process.arch === 'arm64' ? '--arm64' : '--amd64';
|
||||
throw createStacklessError(utils.wrapInASCIIBox([
|
||||
`You are in DEVELOPMENT mode!`,
|
||||
|
|
@ -91,8 +91,7 @@ async function buildPlaywrightImage() {
|
|||
` PWTEST_DOCKER_BASE_IMAGE=playwright:localbuild npx playwright docker build`,
|
||||
].join('\n'), 1));
|
||||
}
|
||||
baseImageName = process.env.PWTEST_DOCKER_BASE_IMAGE;
|
||||
} else {
|
||||
baseImageName = `mcr.microsoft.com/playwright:v${getPlaywrightVersion()}-${VRT_IMAGE_DISTRO}`;
|
||||
const { code } = await spawnAsync('docker', ['pull', baseImageName], { stdio: 'inherit' });
|
||||
if (code !== 0)
|
||||
throw new Error('Failed to pull docker image!');
|
||||
|
|
@ -286,6 +285,7 @@ export function addDockerCLI(program: Command) {
|
|||
await buildPlaywrightImage();
|
||||
} catch (e) {
|
||||
console.error(e.stack ? e : e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -296,6 +296,7 @@ export function addDockerCLI(program: Command) {
|
|||
await startPlaywrightContainer();
|
||||
} catch (e) {
|
||||
console.error(e.stack ? e : e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -306,6 +307,7 @@ export function addDockerCLI(program: Command) {
|
|||
await stopAllPlaywrightContainers();
|
||||
} catch (e) {
|
||||
console.error(e.stack ? e : e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -316,11 +318,12 @@ export function addDockerCLI(program: Command) {
|
|||
await deletePlaywrightImage();
|
||||
} catch (e) {
|
||||
console.error(e.stack ? e : e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
dockerCommand.command('install-server-deps', { hidden: true })
|
||||
.description('delete docker image, if any')
|
||||
.description('install run-server dependencies')
|
||||
.action(async function() {
|
||||
const { code } = await spawnAsync('bash', [path.join(__dirname, '..', '..', 'bin', 'container_install_deps.sh')], { stdio: 'inherit' });
|
||||
if (code !== 0)
|
||||
|
|
@ -328,7 +331,7 @@ export function addDockerCLI(program: Command) {
|
|||
});
|
||||
|
||||
dockerCommand.command('run-server', { hidden: true })
|
||||
.description('delete docker image, if any')
|
||||
.description('run playwright server')
|
||||
.action(async function() {
|
||||
await spawnAsync('bash', [path.join(__dirname, '..', '..', 'bin', 'container_run_server.sh')], { stdio: 'inherit' });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -334,6 +334,7 @@ scheme.RecorderSource = tObject({
|
|||
scheme.DebugControllerInitializer = tOptional(tObject({}));
|
||||
scheme.DebugControllerInspectRequestedEvent = tObject({
|
||||
selector: tString,
|
||||
locators: tArray(tType('NameValue')),
|
||||
});
|
||||
scheme.DebugControllerBrowsersChangedEvent = tObject({
|
||||
browsers: tArray(tObject({
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ import type { InstrumentationListener } from './instrumentation';
|
|||
import type { Playwright } from './playwright';
|
||||
import { Recorder } from './recorder';
|
||||
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();
|
||||
|
||||
|
|
@ -215,7 +218,8 @@ class InspectingRecorderApp extends EmptyRecorderApp {
|
|||
}
|
||||
|
||||
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> {
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ export class DebugControllerDispatcher extends Dispatcher<DebugController, chann
|
|||
this._object.on(DebugController.Events.BrowsersChanged, browsers => {
|
||||
this._dispatchEvent('browsersChanged', { browsers });
|
||||
});
|
||||
this._object.on(DebugController.Events.InspectRequested, selector => {
|
||||
this._dispatchEvent('inspectRequested', { selector });
|
||||
this._object.on(DebugController.Events.InspectRequested, ({ selector, locators }) => {
|
||||
this._dispatchEvent('inspectRequested', { selector, locators });
|
||||
});
|
||||
this._object.on(DebugController.Events.SourcesChanged, sources => {
|
||||
this._dispatchEvent('sourcesChanged', { sources });
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
import { stringifySelector } from '../isomorphic/selectorParser';
|
||||
import type { ParsedSelector } from '../isomorphic/selectorParser';
|
||||
import type { InjectedScript } from './injectedScript';
|
||||
import { asLocator } from '../isomorphic/locatorGenerators';
|
||||
import type { Language } from '../isomorphic/locatorGenerators';
|
||||
|
||||
type HighlightEntry = {
|
||||
targetElement: Element,
|
||||
|
|
@ -35,6 +37,7 @@ export class Highlight {
|
|||
private _isUnderTest: boolean;
|
||||
private _injectedScript: InjectedScript;
|
||||
private _rafRequest: number | undefined;
|
||||
private _language: Language = 'javascript';
|
||||
|
||||
constructor(injectedScript: InjectedScript) {
|
||||
this._injectedScript = injectedScript;
|
||||
|
|
@ -102,6 +105,10 @@ export class Highlight {
|
|||
document.documentElement.appendChild(this._glassPaneElement);
|
||||
}
|
||||
|
||||
setLanguage(language: Language) {
|
||||
this._language = language;
|
||||
}
|
||||
|
||||
runHighlightOnRaf(selector: ParsedSelector) {
|
||||
if (this._rafRequest)
|
||||
cancelAnimationFrame(this._rafRequest);
|
||||
|
|
@ -145,7 +152,7 @@ export class Highlight {
|
|||
color = '#dc6f6f7f';
|
||||
else
|
||||
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[]) {
|
||||
|
|
|
|||
|
|
@ -94,7 +94,8 @@ class Recorder {
|
|||
return;
|
||||
}
|
||||
|
||||
const { mode, actionPoint, actionSelector } = state;
|
||||
const { mode, actionPoint, actionSelector, language } = state;
|
||||
this._highlight.setLanguage(language);
|
||||
if (mode !== this._mode) {
|
||||
this._mode = mode;
|
||||
this._clearHighlight();
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ function buildCandidates(injectedScript: InjectedScript, element: Element, acces
|
|||
const candidates: SelectorToken[] = [];
|
||||
|
||||
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']) {
|
||||
if (element.getAttribute(attr))
|
||||
|
|
@ -158,7 +158,7 @@ function buildCandidates(injectedScript: InjectedScript, element: Element, acces
|
|||
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
|
||||
const input = element as HTMLInputElement | HTMLTextAreaElement;
|
||||
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];
|
||||
if (label) {
|
||||
const labelText = elementText(injectedScript._evaluator._cacheText, label).full.trim();
|
||||
|
|
@ -170,13 +170,13 @@ function buildCandidates(injectedScript: InjectedScript, element: Element, acces
|
|||
if (ariaRole) {
|
||||
const ariaName = getElementAccessibleName(element, false, accessibleNameCache);
|
||||
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
|
||||
candidates.push({ engine: 'role', selector: ariaRole, score: 150 });
|
||||
}
|
||||
|
||||
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))
|
||||
candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[name=${quoteAttributeValue(element.getAttribute('name')!)}]`, score: 50 });
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ import type { CSSComplexSelectorList } from '../isomorphic/cssParser';
|
|||
import { parseAttributeSelector, parseSelector, stringifySelector } 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 LocatorBase = 'page' | 'locator' | 'frame-locator';
|
||||
|
||||
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 {
|
||||
|
|
@ -74,13 +74,14 @@ function innerAsLocator(factory: LocatorFactory, selector: string, isFrameLocato
|
|||
|
||||
if (part.name === 'internal:attr') {
|
||||
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') {
|
||||
tokens.push(factory.generateLocator(base, 'test-id', value));
|
||||
continue;
|
||||
}
|
||||
|
||||
const { exact, text } = detectExact(value);
|
||||
const text = value as string | RegExp;
|
||||
const exact = !!caseSensitive;
|
||||
if (name === 'placeholder') {
|
||||
tokens.push(factory.generateLocator(base, 'placeholder', text, { exact }));
|
||||
continue;
|
||||
|
|
@ -104,8 +105,11 @@ function innerAsLocator(factory: LocatorFactory, selector: string, isFrameLocato
|
|||
return tokens.join('.');
|
||||
}
|
||||
|
||||
function detectExact(text: string): { exact: boolean, text: string } {
|
||||
function detectExact(text: string): { exact?: boolean, text: string | RegExp } {
|
||||
let exact = false;
|
||||
const match = text.match(/^\/(.*)\/([igm]*)$/);
|
||||
if (match)
|
||||
return { text: new RegExp(match[1], match[2]) };
|
||||
if (text.startsWith('"') && text.endsWith('"')) {
|
||||
text = JSON.parse(text);
|
||||
exact = true;
|
||||
|
|
@ -114,10 +118,10 @@ function detectExact(text: string): { exact: boolean, text: string } {
|
|||
}
|
||||
|
||||
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) {
|
||||
case 'default':
|
||||
return `locator(${this.quote(body)})`;
|
||||
return `locator(${this.quote(body as string)})`;
|
||||
case 'nth':
|
||||
return `nth(${body})`;
|
||||
case 'first':
|
||||
|
|
@ -129,11 +133,11 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
|||
for (const [name, value] of Object.entries(options.attrs!))
|
||||
attrs.push(`${name}: ${typeof value === 'string' ? this.quote(value) : value}`);
|
||||
const attrString = attrs.length ? `, { ${attrs.join(', ')} }` : '';
|
||||
return `getByRole(${this.quote(body)}${attrString})`;
|
||||
return `getByRole(${this.quote(body as string)}${attrString})`;
|
||||
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':
|
||||
return `getByTestId(${this.quote(body)})`;
|
||||
return `getByTestId(${this.quote(body as string)})`;
|
||||
case 'text':
|
||||
return this.toCallWithExact('getByText', body, !!options.exact);
|
||||
case 'alt':
|
||||
|
|
@ -149,8 +153,8 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private toCallWithExact(method: string, body: string, exact: boolean) {
|
||||
if (body.startsWith('/') && (body.endsWith('/') || body.endsWith('/i')))
|
||||
private toCallWithExact(method: string, body: string | RegExp, exact?: boolean) {
|
||||
if (isRegExp(body))
|
||||
return `${method}(${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 {
|
||||
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) {
|
||||
case 'default':
|
||||
return `locator(${this.quote(body)})`;
|
||||
return `locator(${this.quote(body as string)})`;
|
||||
case 'nth':
|
||||
return `nth(${body})`;
|
||||
case 'first':
|
||||
|
|
@ -176,11 +180,11 @@ export class PythonLocatorFactory implements LocatorFactory {
|
|||
for (const [name, value] of Object.entries(options.attrs!))
|
||||
attrs.push(`${toSnakeCase(name)}=${typeof value === 'string' ? this.quote(value) : value}`);
|
||||
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':
|
||||
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':
|
||||
return `get_by_test_id(${this.quote(body)})`;
|
||||
return `get_by_test_id(${this.quote(body as string)})`;
|
||||
case 'text':
|
||||
return this.toCallWithExact('get_by_text', body, !!options.exact);
|
||||
case 'alt':
|
||||
|
|
@ -196,11 +200,10 @@ export class PythonLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private toCallWithExact(method: string, body: string, exact: boolean) {
|
||||
if (body.startsWith('/') && (body.endsWith('/') || body.endsWith('/i'))) {
|
||||
const regex = body.substring(1, body.lastIndexOf('/'));
|
||||
const suffix = body.endsWith('i') ? ', re.IGNORECASE' : '';
|
||||
return `${method}(re.compile(r${this.quote(regex)}${suffix}))`;
|
||||
private toCallWithExact(method: string, body: string | RegExp, exact: boolean) {
|
||||
if (isRegExp(body)) {
|
||||
const suffix = body.flags.includes('i') ? ', re.IGNORECASE' : '';
|
||||
return `${method}(re.compile(r${this.quote(body.source)}${suffix}))`;
|
||||
}
|
||||
if (exact)
|
||||
return `${method}(${this.quote(body)}, exact=true)`;
|
||||
|
|
@ -213,7 +216,7 @@ export class PythonLocatorFactory 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;
|
||||
switch (base) {
|
||||
case 'page': clazz = 'Page'; break;
|
||||
|
|
@ -222,7 +225,7 @@ export class JavaLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
switch (kind) {
|
||||
case 'default':
|
||||
return `locator(${this.quote(body)})`;
|
||||
return `locator(${this.quote(body as string)})`;
|
||||
case 'nth':
|
||||
return `nth(${body})`;
|
||||
case 'first':
|
||||
|
|
@ -234,11 +237,11 @@ export class JavaLocatorFactory implements LocatorFactory {
|
|||
for (const [name, value] of Object.entries(options.attrs!))
|
||||
attrs.push(`.set${toTitleCase(name)}(${typeof value === 'string' ? this.quote(value) : value})`);
|
||||
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':
|
||||
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':
|
||||
return `getByTestId(${this.quote(body)})`;
|
||||
return `getByTestId(${this.quote(body as string)})`;
|
||||
case 'text':
|
||||
return this.toCallWithExact(clazz, 'getByText', body, !!options.exact);
|
||||
case 'alt':
|
||||
|
|
@ -254,11 +257,10 @@ export class JavaLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private toCallWithExact(clazz: string, method: string, body: string, exact: boolean) {
|
||||
if (body.startsWith('/') && (body.endsWith('/') || body.endsWith('/i'))) {
|
||||
const regex = body.substring(1, body.lastIndexOf('/'));
|
||||
const suffix = body.endsWith('i') ? ', Pattern.CASE_INSENSITIVE' : '';
|
||||
return `${method}(Pattern.compile(${this.quote(regex)}${suffix}))`;
|
||||
private toCallWithExact(clazz: string, method: string, body: string | RegExp, exact: boolean) {
|
||||
if (isRegExp(body)) {
|
||||
const suffix = body.flags.includes('i') ? ', Pattern.CASE_INSENSITIVE' : '';
|
||||
return `${method}(Pattern.compile(${this.quote(body.source)}${suffix}))`;
|
||||
}
|
||||
if (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 {
|
||||
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) {
|
||||
case 'default':
|
||||
return `Locator(${this.quote(body)})`;
|
||||
return `Locator(${this.quote(body as string)})`;
|
||||
case 'nth':
|
||||
return `Nth(${body})`;
|
||||
case 'first':
|
||||
|
|
@ -283,14 +285,16 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
|||
return `Last`;
|
||||
case 'role':
|
||||
const attrs: string[] = [];
|
||||
for (const [name, value] of Object.entries(options.attrs!))
|
||||
attrs.push(`${toTitleCase(name)} = ${typeof value === 'string' ? this.quote(value) : value}`);
|
||||
const attrString = attrs.length ? `, new () { ${attrs.join(', ')} }` : '';
|
||||
return `GetByRole(${this.quote(body)}${attrString})`;
|
||||
for (const [name, value] of Object.entries(options.attrs!)) {
|
||||
const optionKey = name === 'name' ? 'NameString' : toTitleCase(name);
|
||||
attrs.push(`${optionKey} = ${typeof value === 'string' ? this.quote(value) : value}`);
|
||||
}
|
||||
const attrString = attrs.length ? `, new() { ${attrs.join(', ')} }` : '';
|
||||
return `GetByRole(AriaRole.${toTitleCase(body as string)}${attrString})`;
|
||||
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':
|
||||
return `GetByTestId(${this.quote(body)})`;
|
||||
return `GetByTestId(${this.quote(body as string)})`;
|
||||
case 'text':
|
||||
return this.toCallWithExact('GetByText', body, !!options.exact);
|
||||
case 'alt':
|
||||
|
|
@ -306,14 +310,13 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private toCallWithExact(method: string, body: string, exact: boolean) {
|
||||
if (body.startsWith('/') && (body.endsWith('/') || body.endsWith('/i'))) {
|
||||
const regex = body.substring(1, body.lastIndexOf('/'));
|
||||
const suffix = body.endsWith('i') ? ', RegexOptions.IgnoreCase' : '';
|
||||
return `${method}(new Regex(${this.quote(regex)}${suffix}))`;
|
||||
private toCallWithExact(method: string, body: string | RegExp, exact: boolean) {
|
||||
if (isRegExp(body)) {
|
||||
const suffix = body.flags.includes('i') ? ', RegexOptions.IgnoreCase' : '';
|
||||
return `${method}(new Regex(${this.quote(body.source)}${suffix}))`;
|
||||
}
|
||||
if (exact)
|
||||
return `${method}(${this.quote(body)}, new () { Exact: true })`;
|
||||
return `${method}(${this.quote(body)}, new() { Exact: true })`;
|
||||
return `${method}(${this.quote(body)})`;
|
||||
}
|
||||
|
||||
|
|
@ -328,3 +331,7 @@ const generators: Record<Language, LocatorFactory> = {
|
|||
java: new JavaLocatorFactory(),
|
||||
csharp: new CSharpLocatorFactory(),
|
||||
};
|
||||
|
||||
export function isRegExp(obj: any): obj is RegExp {
|
||||
return obj instanceof RegExp;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import { metadataToCallLog } from './recorder/recorderUtils';
|
|||
import { Debugger } from './debugger';
|
||||
import { EventEmitter } from 'events';
|
||||
import { raceAgainstTimeout } from '../utils/timeoutRunner';
|
||||
import type { LanguageGenerator } from './recorder/language';
|
||||
import type { Language, LanguageGenerator } from './recorder/language';
|
||||
|
||||
type BindingSource = { frame: Frame, page: Page };
|
||||
|
||||
|
|
@ -59,6 +59,7 @@ export class Recorder implements InstrumentationListener {
|
|||
private _handleSIGINT: boolean | undefined;
|
||||
private _recorderAppFactory: (recorder: Recorder) => Promise<IRecorderApp>;
|
||||
private _omitCallTracking = false;
|
||||
private _currentLanguage: Language;
|
||||
|
||||
static showInspector(context: BrowserContext) {
|
||||
Recorder.show(context, {}).catch(() => {});
|
||||
|
|
@ -83,6 +84,7 @@ export class Recorder implements InstrumentationListener {
|
|||
this._debugger = Debugger.lookup(context)!;
|
||||
this._handleSIGINT = params.handleSIGINT;
|
||||
context.instrumentation.addListener(this, context);
|
||||
this._currentLanguage = this._contextRecorder.languageName();
|
||||
}
|
||||
|
||||
private static async defaultRecorderAppFactory(recorder: Recorder) {
|
||||
|
|
@ -111,6 +113,11 @@ export class Recorder implements InstrumentationListener {
|
|||
this._debugger.resume(true);
|
||||
return;
|
||||
}
|
||||
if (data.event === 'fileChanged') {
|
||||
this._currentLanguage = this._contextRecorder.languageName(data.params.file);
|
||||
this._refreshOverlay();
|
||||
return;
|
||||
}
|
||||
if (data.event === 'resume') {
|
||||
this._debugger.resume(false);
|
||||
return;
|
||||
|
|
@ -155,6 +162,7 @@ export class Recorder implements InstrumentationListener {
|
|||
mode: this._mode,
|
||||
actionPoint,
|
||||
actionSelector,
|
||||
language: this._currentLanguage
|
||||
};
|
||||
return uiState;
|
||||
});
|
||||
|
|
@ -381,6 +389,14 @@ class ContextRecorder extends EventEmitter {
|
|||
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() {
|
||||
this._context.on(BrowserContext.Events.Page, page => this._onPage(page));
|
||||
for (const page of this._context.pages())
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import type { BrowserContextOptions } from '../../..';
|
||||
import type { LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import { sanitizeDeviceOptions, toSignalMap } from './language';
|
||||
import type { ActionInContext } from './codeGenerator';
|
||||
import type { Action } from './recorderActions';
|
||||
|
|
@ -31,7 +31,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
id: string;
|
||||
groupName = '.NET C#';
|
||||
name: string;
|
||||
highlighter = 'csharp';
|
||||
highlighter = 'csharp' as Language;
|
||||
_mode: CSharpLanguageMode;
|
||||
|
||||
constructor(mode: CSharpLanguageMode) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import type { BrowserContextOptions } from '../../..';
|
||||
import type { LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import { toSignalMap } from './language';
|
||||
import type { ActionInContext } from './codeGenerator';
|
||||
import type { Action } from './recorderActions';
|
||||
|
|
@ -30,7 +30,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
|||
id = 'java';
|
||||
groupName = 'Java';
|
||||
name = 'Library';
|
||||
highlighter = 'java';
|
||||
highlighter = 'java' as Language;
|
||||
|
||||
generateAction(actionInContext: ActionInContext): string {
|
||||
const action = actionInContext.action;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import type { BrowserContextOptions } from '../../..';
|
||||
import type { LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import { sanitizeDeviceOptions, toSignalMap } from './language';
|
||||
import type { ActionInContext } from './codeGenerator';
|
||||
import type { Action } from './recorderActions';
|
||||
|
|
@ -29,7 +29,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
id: string;
|
||||
groupName = 'Node.js';
|
||||
name: string;
|
||||
highlighter = 'javascript';
|
||||
highlighter = 'javascript' as Language;
|
||||
private _isTest: boolean;
|
||||
|
||||
constructor(isTest: boolean) {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@
|
|||
*/
|
||||
|
||||
import type { BrowserContextOptions, LaunchOptions } from '../../..';
|
||||
import type { Language } from '../isomorphic/locatorGenerators';
|
||||
import type { ActionInContext } from './codeGenerator';
|
||||
import type { Action, DialogSignal, DownloadSignal, NavigationSignal, PopupSignal } from './recorderActions';
|
||||
export type { Language } from '../isomorphic/locatorGenerators';
|
||||
|
||||
export type LanguageGeneratorOptions = {
|
||||
browserName: string;
|
||||
|
|
@ -33,7 +35,7 @@ export interface LanguageGenerator {
|
|||
id: string;
|
||||
groupName: string;
|
||||
name: string;
|
||||
highlighter: string;
|
||||
highlighter: Language;
|
||||
generateHeader(options: LanguageGeneratorOptions): string;
|
||||
generateAction(actionInContext: ActionInContext): string;
|
||||
generateFooter(saveStorage: string | undefined): string;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import type { BrowserContextOptions } from '../../..';
|
||||
import type { LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import { sanitizeDeviceOptions, toSignalMap } from './language';
|
||||
import type { ActionInContext } from './codeGenerator';
|
||||
import type { Action } from './recorderActions';
|
||||
|
|
@ -29,7 +29,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
id: string;
|
||||
groupName = 'Python';
|
||||
name: string;
|
||||
highlighter = 'python';
|
||||
highlighter = 'python' as Language;
|
||||
|
||||
private _awaitPrefix: '' | 'await ';
|
||||
private _asyncPrefix: '' | 'async ';
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ export function toTitleCase(name: string) {
|
|||
}
|
||||
|
||||
export function toSnakeCase(name: string): string {
|
||||
const toSnakeCaseRegex = /((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))/g;
|
||||
return name.replace(toSnakeCaseRegex, `_$1`).toLowerCase();
|
||||
return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
||||
}
|
||||
|
||||
export function cssEscape(s: string): string {
|
||||
|
|
@ -69,14 +68,14 @@ export function escapeForTextSelector(text: string | RegExp, exact: boolean, cas
|
|||
if (exact)
|
||||
return '"' + text.replace(/["]/g, '\\"') + '"';
|
||||
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;
|
||||
}
|
||||
|
||||
export function escapeForAttributeSelector(value: string): string {
|
||||
export function escapeForAttributeSelector(value: string, exact: boolean): string {
|
||||
// TODO: this should actually be
|
||||
// cssEscape(value).replace(/\\ /g, ' ')
|
||||
// However, our attribute selectors do not conform to CSS parsing spec,
|
||||
// so we escape them differently.
|
||||
return `"${value.replace(/["]/g, '\\"')}"`;
|
||||
return `"${value.replace(/["]/g, '\\"')}"${exact ? '' : 'i'}`;
|
||||
}
|
||||
|
|
|
|||
204
packages/playwright-core/types/types.d.ts
vendored
204
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -2457,7 +2457,8 @@ export interface Page {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -2476,7 +2477,8 @@ export interface Page {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -2494,7 +2496,8 @@ export interface Page {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -2512,7 +2515,7 @@ export interface Page {
|
|||
* @param role Required aria role.
|
||||
* @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
|
||||
* checked are `true`, `false` and `"mixed"`.
|
||||
|
|
@ -2583,13 +2586,46 @@ export interface Page {
|
|||
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 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;
|
||||
}): Locator;
|
||||
|
|
@ -2606,7 +2642,8 @@ export interface Page {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -5543,7 +5580,8 @@ export interface Frame {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -5562,7 +5600,8 @@ export interface Frame {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -5580,7 +5619,8 @@ export interface Frame {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -5598,7 +5638,7 @@ export interface Frame {
|
|||
* @param role Required aria role.
|
||||
* @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
|
||||
* checked are `true`, `false` and `"mixed"`.
|
||||
|
|
@ -5669,13 +5709,46 @@ export interface Frame {
|
|||
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 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;
|
||||
}): Locator;
|
||||
|
|
@ -5692,7 +5765,8 @@ export interface Frame {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -9966,7 +10040,8 @@ export interface Locator {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -9985,7 +10060,8 @@ export interface Locator {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -10003,7 +10079,8 @@ export interface Locator {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -10021,7 +10098,7 @@ export interface Locator {
|
|||
* @param role Required aria role.
|
||||
* @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
|
||||
* checked are `true`, `false` and `"mixed"`.
|
||||
|
|
@ -10092,13 +10169,46 @@ export interface 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 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;
|
||||
}): Locator;
|
||||
|
|
@ -10115,7 +10225,8 @@ export interface Locator {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -15221,7 +15332,8 @@ export interface FrameLocator {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -15240,7 +15352,8 @@ export interface FrameLocator {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -15258,7 +15371,8 @@ export interface FrameLocator {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
@ -15276,7 +15390,7 @@ export interface FrameLocator {
|
|||
* @param role Required aria role.
|
||||
* @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
|
||||
* checked are `true`, `false` and `"mixed"`.
|
||||
|
|
@ -15347,13 +15461,46 @@ export interface FrameLocator {
|
|||
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 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;
|
||||
}): Locator;
|
||||
|
|
@ -15370,7 +15517,8 @@ export interface FrameLocator {
|
|||
*/
|
||||
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;
|
||||
}): Locator;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-react",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"description": "Playwright Component Testing for React",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
"@playwright/test": "1.27.0-next",
|
||||
"@playwright/test": "1.27.1",
|
||||
"vite": "^3.0.9"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-solid",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"description": "Playwright Component Testing for Solid",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
"dependencies": {
|
||||
"vite": "^3.0.0",
|
||||
"vite-plugin-solid": "^2.3.0",
|
||||
"@playwright/test": "1.27.0-next"
|
||||
"@playwright/test": "1.27.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"solid-js": "^1.4.7"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-svelte",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"description": "Playwright Component Testing for Svelte",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.27.0-next",
|
||||
"@playwright/test": "1.27.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.0.1",
|
||||
"vite": "^3.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-vue",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"description": "Playwright Component Testing for Vue",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^2.3.1",
|
||||
"@playwright/test": "1.27.0-next",
|
||||
"@playwright/test": "1.27.1",
|
||||
"vite": "^2.9.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-vue2",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"description": "Playwright Component Testing for Vue2",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.27.0-next",
|
||||
"@playwright/test": "1.27.1",
|
||||
"vite": "^2.9.5",
|
||||
"vite-plugin-vue2": "^2.0.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-firefox",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"description": "A high-level API to automate Firefox",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,6 +28,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/test",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -34,6 +34,6 @@
|
|||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { transformHook, resolveHook, belongsToNodeModules } from './transform';
|
|||
async function resolve(specifier: string, context: { parentURL?: string }, defaultResolve: Function) {
|
||||
if (context.parentURL && context.parentURL.startsWith('file://')) {
|
||||
const filename = url.fileURLToPath(context.parentURL);
|
||||
const resolved = resolveHook(filename, specifier);
|
||||
const resolved = resolveHook(true, filename, specifier);
|
||||
if (resolved !== undefined)
|
||||
specifier = url.pathToFileURL(resolved).toString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -579,7 +579,9 @@ type ParsedStackTrace = {
|
|||
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;
|
||||
if (videoMode === 'retry-with-video')
|
||||
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));
|
||||
}
|
||||
|
||||
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;
|
||||
if (traceMode === 'retry-with-trace')
|
||||
traceMode = 'on-first-retry';
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ const scriptPreprocessor = process.env.PW_TEST_SOURCE_TRANSFORM ?
|
|||
require(process.env.PW_TEST_SOURCE_TRANSFORM) : undefined;
|
||||
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))
|
||||
return;
|
||||
const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx');
|
||||
|
|
@ -132,6 +132,15 @@ export function resolveHook(filename: string, specifier: string): string | undef
|
|||
if (value.includes('*'))
|
||||
candidate = candidate.replace('*', matchedPartOfSpecifier);
|
||||
candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate.replace(/\//g, path.sep));
|
||||
if (isModule) {
|
||||
const transformed = js2ts(candidate);
|
||||
if (transformed || fs.existsSync(candidate)) {
|
||||
if (keyPrefix.length > longestPrefixLength) {
|
||||
longestPrefixLength = keyPrefix.length;
|
||||
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) {
|
||||
|
|
@ -142,17 +151,22 @@ export function resolveHook(filename: string, specifier: string): string | undef
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pathMatchedByLongestPrefix)
|
||||
return pathMatchedByLongestPrefix;
|
||||
}
|
||||
if (specifier.endsWith('.js')) {
|
||||
const resolved = path.resolve(path.dirname(filename), specifier);
|
||||
if (resolved.endsWith('.js')) {
|
||||
const tsResolved = resolved.substring(0, resolved.length - 3) + '.ts';
|
||||
|
||||
if (isModule)
|
||||
return js2ts(path.resolve(path.dirname(filename), specifier));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function transformHook(code: string, filename: string, moduleUrl?: string): string {
|
||||
|
|
@ -197,7 +211,7 @@ export function installTransform(): () => void {
|
|||
const originalResolveFilename = (Module as any)._resolveFilename;
|
||||
function resolveFilename(this: any, specifier: string, parent: Module, ...rest: any[]) {
|
||||
if (!reverted && parent) {
|
||||
const resolved = resolveHook(parent.filename, specifier);
|
||||
const resolved = resolveHook(false, parent.filename, specifier);
|
||||
if (resolved !== undefined)
|
||||
specifier = resolved;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-webkit",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"description": "A high-level API to automate WebKit",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,6 +28,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright",
|
||||
"version": "1.27.0-next",
|
||||
"version": "1.27.1",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,6 +28,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.27.0-next"
|
||||
"playwright-core": "1.27.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -610,6 +610,7 @@ export interface DebugControllerChannel extends DebugControllerEventTarget, Chan
|
|||
}
|
||||
export type DebugControllerInspectRequestedEvent = {
|
||||
selector: string,
|
||||
locators: NameValue[],
|
||||
};
|
||||
export type DebugControllerBrowsersChangedEvent = {
|
||||
browsers: {
|
||||
|
|
|
|||
|
|
@ -693,6 +693,9 @@ DebugController:
|
|||
inspectRequested:
|
||||
parameters:
|
||||
selector: string
|
||||
locators:
|
||||
type: array
|
||||
items: NameValue
|
||||
|
||||
browsersChanged:
|
||||
parameters:
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||
<div>Target:</div>
|
||||
<select className='recorder-chooser' hidden={!sources.length} value={fileId} onChange={event => {
|
||||
setFileId(event.target.selectedOptions[0].value);
|
||||
window.dispatch({ event: 'fileChanged', params: { file: event.target.selectedOptions[0].value } });
|
||||
}}>{renderSourceOptions(sources)}</select>
|
||||
<ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => {
|
||||
window.dispatch({ event: 'clear' });
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export type Point = { x: number, y: number };
|
|||
export type Mode = 'inspecting' | 'recording' | 'none';
|
||||
|
||||
export type EventData = {
|
||||
event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode' | 'selectorUpdated';
|
||||
event: 'clear' | 'resume' | 'step' | 'pause' | 'setMode' | 'selectorUpdated' | 'fileChanged';
|
||||
params: any;
|
||||
};
|
||||
|
||||
|
|
@ -27,6 +27,7 @@ export type UIState = {
|
|||
mode: Mode;
|
||||
actionPoint?: Point;
|
||||
actionSelector?: string;
|
||||
language: 'javascript' | 'python' | 'java' | 'csharp';
|
||||
};
|
||||
|
||||
export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused';
|
||||
|
|
|
|||
|
|
@ -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.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) => {
|
||||
res.setHeader('Set-Cookie', 'name=value; Path=/');
|
||||
res.end();
|
||||
|
|
@ -372,7 +374,6 @@ it('should support requestStorageAccess', async ({ page, server, browserName, is
|
|||
]);
|
||||
expect(serverRequest.headers.cookie).toBe('name=value');
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
if (isLinux && browserName === 'webkit')
|
||||
expect(await frame.evaluate(() => document.hasStorageAccess())).toBeTruthy();
|
||||
|
|
|
|||
|
|
@ -34,20 +34,20 @@ test.describe('cli codegen', () => {
|
|||
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();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
expect.soft(sources.get('Python').text).toContain(`
|
||||
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()`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
page.getByRole("button", new Page.GetByRoleOptions().setName("Submit")).click()`);
|
||||
expect.soft(sources.get('Java').text).toContain(`
|
||||
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit")).click()`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
await page.GetByRole("button", new () { Name = "Submit" }).ClickAsync();`);
|
||||
expect.soft(sources.get('C#').text).toContain(`
|
||||
await page.GetByRole(AriaRole.Button, new() { NameString = "Submit" }).ClickAsync();`);
|
||||
|
||||
expect(message.text()).toBe('click');
|
||||
});
|
||||
|
|
@ -157,20 +157,20 @@ test.describe('cli codegen', () => {
|
|||
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();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
expect.soft(sources.get('Python').text).toContain(`
|
||||
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()`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
page.getByRole("button", new Page.GetByRoleOptions().setName("Submit")).click()`);
|
||||
expect.soft(sources.get('Java').text).toContain(`
|
||||
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit")).click()`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
await page.GetByRole("button", new () { Name = "Submit" }).ClickAsync();`);
|
||||
expect.soft(sources.get('C#').text).toContain(`
|
||||
await page.GetByRole(AriaRole.Button, new() { NameString = "Submit" }).ClickAsync();`);
|
||||
|
||||
expect(message.text()).toBe('click');
|
||||
});
|
||||
|
|
@ -548,31 +548,31 @@ test.describe('cli codegen', () => {
|
|||
page.dispatchEvent('a', 'click', { detail: 1 })
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`
|
||||
const [page1] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
page.getByRole('link', { name: 'link' }).click()
|
||||
]);`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
expect.soft(sources.get('Java').text).toContain(`
|
||||
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:
|
||||
page.get_by_role("link", name="link").click()
|
||||
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:
|
||||
await page.get_by_role("link", name="link").click()
|
||||
page1 = await popup_info.value`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
expect.soft(sources.get('C#').text).toContain(`
|
||||
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');
|
||||
|
|
|
|||
|
|
@ -226,41 +226,41 @@ test.describe('cli codegen', () => {
|
|||
]);
|
||||
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();`);
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
page.getByRole('link', { name: 'Download' }).click()
|
||||
]);`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
expect.soft(sources.get('Java').text).toContain(`
|
||||
BrowserContext context = browser.newContext();`);
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
expect.soft(sources.get('Java').text).toContain(`
|
||||
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()`);
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
expect.soft(sources.get('Python').text).toContain(`
|
||||
with page.expect_download() as download_info:
|
||||
page.get_by_role("link", name="Download").click()
|
||||
download = download_info.value`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
expect.soft(sources.get('Python Async').text).toContain(`
|
||||
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:
|
||||
await page.get_by_role("link", name="Download").click()
|
||||
download = await download_info.value`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
expect.soft(sources.get('C#').text).toContain(`
|
||||
var context = await browser.NewContextAsync();`);
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
expect.soft(sources.get('C#').text).toContain(`
|
||||
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');
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`
|
||||
page.once('dialog', dialog => {
|
||||
console.log(\`Dialog message: \${dialog.message()}\`);
|
||||
dialog.dismiss().catch(() => {});
|
||||
});
|
||||
await page.getByRole('button', { name: 'click me' }).click();`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
expect.soft(sources.get('Java').text).toContain(`
|
||||
page.onceDialog(dialog -> {
|
||||
System.out.println(String.format("Dialog message: %s", dialog.message()));
|
||||
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.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())
|
||||
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)
|
||||
{
|
||||
Console.WriteLine($\"Dialog message: {dialog.Message}\");
|
||||
|
|
@ -308,7 +308,7 @@ test.describe('cli codegen', () => {
|
|||
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();`);
|
||||
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -46,10 +46,10 @@ test.describe('cli codegen', () => {
|
|||
await page.get_by_role("button", name="Submit").first.click()`);
|
||||
|
||||
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(`
|
||||
await page.GetByRole("button", new () { Name = "Submit" }).First.ClickAsync();`);
|
||||
await page.GetByRole(AriaRole.Button, new() { NameString = "Submit" }).First.ClickAsync();`);
|
||||
|
||||
expect(message.text()).toBe('click1');
|
||||
});
|
||||
|
|
@ -81,10 +81,10 @@ test.describe('cli codegen', () => {
|
|||
await page.get_by_role("button", name="Submit").nth(1).click()`);
|
||||
|
||||
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(`
|
||||
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');
|
||||
});
|
||||
|
|
@ -217,7 +217,7 @@ test.describe('cli codegen', () => {
|
|||
await page.frameLocator('#frame1').getByRole('button', { name: 'Submit' }).click();`);
|
||||
|
||||
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(`
|
||||
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()`);
|
||||
|
||||
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 }) => {
|
||||
|
|
@ -267,7 +267,7 @@ test.describe('cli codegen', () => {
|
|||
await recorder.setContentAndWait(`<input placeholder="Country"></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([
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
|
|
@ -296,7 +296,7 @@ test.describe('cli codegen', () => {
|
|||
await recorder.setContentAndWait(`<input alt="Country"></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([
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
|
|
|
|||
138
tests/library/locator-generator.spec.ts
Normal file
138
tests/library/locator-generator.spec.ts
Normal 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))',
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -45,7 +45,7 @@ it.describe('selector generator', () => {
|
|||
|
||||
it('should not escape spaces inside named attr selectors', async ({ page }) => {
|
||||
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 }) => {
|
||||
|
|
@ -232,7 +232,7 @@ it.describe('selector generator', () => {
|
|||
});
|
||||
it('placeholder', async ({ page }) => {
|
||||
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 }) => {
|
||||
await page.setContent(`<input type="text"/>`);
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ it('should highlight locator', async ({ page, isAndroid }) => {
|
|||
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: ');
|
||||
await page.locator('input').highlight();
|
||||
expect(await textPromise).toBe('input');
|
||||
expect(await textPromise).toBe('locator(\'input\')');
|
||||
let box1 = await page.locator('input').boundingBox();
|
||||
let box2 = await boxPromise;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>');
|
||||
|
||||
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 }) => {
|
||||
|
|
@ -108,29 +108,31 @@ it('getByTitle should work', 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 />`);
|
||||
await page.$eval('input', input => {
|
||||
input.setAttribute('placeholder', 'hello\nwo"rld');
|
||||
input.setAttribute('title', 'hello\nwo"rld');
|
||||
input.setAttribute('alt', 'hello\nwo"rld');
|
||||
input.setAttribute('placeholder', 'hello my\nwo"rld');
|
||||
input.setAttribute('title', 'hello my\nwo"rld');
|
||||
input.setAttribute('alt', 'hello my\nwo"rld');
|
||||
});
|
||||
await expect(page.getByText('hello\nwo"rld')).toHaveAttribute('id', 'label');
|
||||
await expect(page.getByLabel('hello\nwo"rld')).toHaveAttribute('id', 'control');
|
||||
await expect(page.getByPlaceholder('hello\nwo"rld')).toHaveAttribute('id', 'control');
|
||||
await expect(page.getByAltText('hello\nwo"rld')).toHaveAttribute('id', 'control');
|
||||
await expect(page.getByTitle('hello\nwo"rld')).toHaveAttribute('id', 'control');
|
||||
await expect(page.getByText('hello my\nwo"rld')).toHaveAttribute('id', 'label');
|
||||
await expect(page.getByText('hello my wo"rld')).toHaveAttribute('id', 'label');
|
||||
await expect(page.getByLabel('hello my\nwo"rld')).toHaveAttribute('id', 'control');
|
||||
await expect(page.getByPlaceholder('hello my\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 />`);
|
||||
await page.$eval('input', input => {
|
||||
input.setAttribute('placeholder', 'hello\nworld');
|
||||
input.setAttribute('title', 'hello\nworld');
|
||||
input.setAttribute('alt', 'hello\nworld');
|
||||
input.setAttribute('placeholder', 'hello my\nworld');
|
||||
input.setAttribute('title', 'hello my\nworld');
|
||||
input.setAttribute('alt', 'hello my\nworld');
|
||||
});
|
||||
await expect(page.getByText('hello\nworld')).toHaveAttribute('id', 'label');
|
||||
await expect(page.getByLabel('hello\nworld')).toHaveAttribute('id', 'control');
|
||||
await expect(page.getByPlaceholder('hello\nworld')).toHaveAttribute('id', 'control');
|
||||
await expect(page.getByAltText('hello\nworld')).toHaveAttribute('id', 'control');
|
||||
await expect(page.getByTitle('hello\nworld')).toHaveAttribute('id', 'control');
|
||||
await expect(page.getByText('hello my\nworld')).toHaveAttribute('id', 'label');
|
||||
await expect(page.getByText('hello my world')).toHaveAttribute('id', 'label');
|
||||
await expect(page.getByLabel('hello my\nworld')).toHaveAttribute('id', 'control');
|
||||
await expect(page.getByPlaceholder('hello my\nworld')).toHaveAttribute('id', 'control');
|
||||
await expect(page.getByAltText('hello my\nworld')).toHaveAttribute('id', 'control');
|
||||
await expect(page.getByTitle('hello my\nworld')).toHaveAttribute('id', 'control');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -360,6 +360,10 @@ test('should support name', async ({ page }) => {
|
|||
`<div role="button" aria-label="Hello"></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([
|
||||
`<div role="button" aria-label="Hello"></div>`,
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ test('should respect path resolver in experimental mode', async ({ runInlineTest
|
|||
},
|
||||
}`,
|
||||
'a.test.ts': `
|
||||
import { foo } from 'util/b.ts';
|
||||
import { foo } from 'util/b.js';
|
||||
const { test } = pwt;
|
||||
test('check project name', ({}, testInfo) => {
|
||||
expect(testInfo.project.name).toBe(foo);
|
||||
|
|
|
|||
|
|
@ -740,11 +740,11 @@ test('test.setTimeout should work separately in afterAll', async ({ runInlineTes
|
|||
});
|
||||
test.afterAll(async () => {
|
||||
console.log('\\n%%afterAll');
|
||||
test.setTimeout(1000);
|
||||
await new Promise(f => setTimeout(f, 800));
|
||||
test.setTimeout(3000);
|
||||
await new Promise(f => setTimeout(f, 2000));
|
||||
});
|
||||
`,
|
||||
}, { timeout: '100' });
|
||||
}, { timeout: '1000' });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([
|
||||
|
|
|
|||
Loading…
Reference in a new issue