Compare commits
13 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a70a96ab25 | ||
|
|
53f51a8cf1 | ||
|
|
2a00ca8453 | ||
|
|
0e6434013b | ||
|
|
cb0f456e46 | ||
|
|
698823a78e | ||
|
|
c0fa804367 | ||
|
|
7a32228aed | ||
|
|
0e31acea8f | ||
|
|
b2a39ffc61 | ||
|
|
1eea46bd66 | ||
|
|
4c53e56cb4 | ||
|
|
3f36d7ff51 |
31
.github/workflows/tests_secondary.yml
vendored
31
.github/workflows/tests_secondary.yml
vendored
|
|
@ -268,29 +268,8 @@ jobs:
|
|||
- run: npx playwright install-deps
|
||||
- run: utils/build/build-playwright-driver.sh
|
||||
|
||||
test_linux_chromium_headless_shell:
|
||||
name: Chromium Headless Shell
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runs-on: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/run-test
|
||||
with:
|
||||
browsers-to-install: chromium chromium-headless-shell
|
||||
command: npm run ctest
|
||||
bot-name: "headless-shell-${{ matrix.runs-on }}"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
env:
|
||||
PWTEST_CHANNEL: chromium-headless-shell
|
||||
|
||||
test_chromium_next:
|
||||
name: Test chromium-next channel
|
||||
test_channel_chromium:
|
||||
name: Test channel=chromium
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -301,11 +280,13 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/run-test
|
||||
with:
|
||||
# TODO: this should pass --no-shell.
|
||||
# However, codegen tests do not inherit the channel and try to launch headless shell.
|
||||
browsers-to-install: chromium
|
||||
command: npm run ctest
|
||||
bot-name: "chromium-next-${{ matrix.runs-on }}"
|
||||
bot-name: "channel-chromium-${{ matrix.runs-on }}"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
env:
|
||||
PWTEST_CHANNEL: chromium-next
|
||||
PWTEST_CHANNEL: chromium
|
||||
|
|
|
|||
|
|
@ -206,6 +206,9 @@ Below is the HTML markup and the respective ARIA snapshot:
|
|||
- link "About"
|
||||
```
|
||||
|
||||
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-%%
|
||||
* since: v1.49
|
||||
|
||||
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-js-%%
|
||||
* since: v1.49
|
||||
|
||||
|
|
|
|||
|
|
@ -2159,3 +2159,6 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
|
|||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
|
||||
* since: v1.49
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.49
|
||||
|
|
|
|||
|
|
@ -338,30 +338,85 @@ dotnet test --settings:webkit.runsettings
|
|||
|
||||
For Google Chrome, Microsoft Edge and other Chromium-based browsers, by default, Playwright uses open source Chromium builds. Since the Chromium project is ahead of the branded browsers, when the world is on Google Chrome N, Playwright already supports Chromium N+1 that will be released in Google Chrome and Microsoft Edge a few weeks later.
|
||||
|
||||
Playwright ships a regular Chromium build for headed operations and a separate [Chromium headless shell](https://developer.chrome.com/blog/chrome-headless-shell) for headless mode. These two behave differently in some edge cases, but the majority of testing scenarios are not affected. Note this behavior has changed in Playwright version 1.49, see [issue #33566](https://github.com/microsoft/playwright/issues/33566) for details.
|
||||
Playwright ships a regular Chromium build for headed operations and a separate [chromium headless shell](https://developer.chrome.com/blog/chrome-headless-shell) for headless mode. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for details.
|
||||
|
||||
#### Save on download size
|
||||
#### Optimize download size on CI
|
||||
|
||||
If you are only running tests in headless, for example on CI, you can avoid downloading a headed version of Chromium by specifying `chromium-headless-shell` during installation.
|
||||
If you are only running tests in headless mode, for example on CI, you can avoid downloading a regular version of Chromium by passing `--only-shell` during installation.
|
||||
|
||||
```bash js
|
||||
# When only running tests headlessly
|
||||
npx playwright install chromium-headless-shell firefox webkit
|
||||
# only running tests headlessly
|
||||
npx playwright install --with-deps --only-shell
|
||||
```
|
||||
|
||||
```bash java
|
||||
# When only running tests headlessly
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install chromium-headless-shell firefox webkit"
|
||||
# only running tests headlessly
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps --only-shell"
|
||||
```
|
||||
|
||||
```bash python
|
||||
# When only running tests headlessly
|
||||
playwright install chromium-headless-shell firefox webkit
|
||||
# only running tests headlessly
|
||||
playwright install --with-deps --only-shell
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
# When only running tests headlessly
|
||||
pwsh bin/Debug/netX/playwright.ps1 install chromium-headless-shell firefox webkit
|
||||
# only running tests headlessly
|
||||
pwsh bin/Debug/netX/playwright.ps1 install --with-deps --only-shell
|
||||
```
|
||||
|
||||
#### Opt-in to new headless mode
|
||||
|
||||
You can opt into the new headless mode by using `'chromium'` channel. As [official Chrome documentation puts it](https://developer.chrome.com/blog/chrome-headless-shell):
|
||||
|
||||
> New Headless on the other hand is the real Chrome browser, and is thus more authentic, reliable, and offers more features. This makes it more suitable for high-accuracy end-to-end web app testing or browser extension testing.
|
||||
|
||||
See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for details.
|
||||
|
||||
```js
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chromium' },
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
```java
|
||||
import com.microsoft.playwright.*;
|
||||
|
||||
public class Example {
|
||||
public static void main(String[] args) {
|
||||
try (Playwright playwright = Playwright.create()) {
|
||||
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setChannel("chromium"));
|
||||
Page page = browser.newPage();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash python
|
||||
pytest test_login.py --browser-channel chromium
|
||||
```
|
||||
|
||||
```xml csharp
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RunSettings>
|
||||
<Playwright>
|
||||
<BrowserName>chromium</BrowserName>
|
||||
<LaunchOptions>
|
||||
<Channel>chromium</Channel>
|
||||
</LaunchOptions>
|
||||
</Playwright>
|
||||
</RunSettings>
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
dotnet test -- Playwright.BrowserName=chromium Playwright.LaunchOptions.Channel=chromium
|
||||
```
|
||||
|
||||
### Google Chrome & Microsoft Edge
|
||||
|
|
|
|||
|
|
@ -40,51 +40,13 @@ Learn more in the [aria snapshots guide](./aria-snapshots).
|
|||
|
||||
### Breaking: channels `chrome`, `msedge` and similar switch to new headless
|
||||
|
||||
Prior to this release, Playwright was running the old established implementation of [Chromium headless mode](https://developer.chrome.com/docs/chromium/headless). However, Chromium had entirely **switched to the new headless mode**, and **removed the old one**.
|
||||
This change affects you if you're using one of the following channels in your `playwright.config.ts`:
|
||||
- `chrome`, `chrome-dev`, `chrome-beta`, or `chrome-canary`
|
||||
- `msedge`, `msedge-dev`, `msedge-beta`, or `msedge-canary`
|
||||
|
||||

|
||||
#### What do I need to do?
|
||||
|
||||
If you are using a browser channel, for example `'chrome'` or `'msedge'`, the headless mode switch **will affect you**. Most likely, you will have to update some of your tests and all of your screenshot expectations. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for more details.
|
||||
|
||||
#### Chromium headless shell
|
||||
|
||||
Starting with this release, Playwright downloads and runs two different browser builds - one is a regular headed chromium and the other is a chromium headless shell. This should be transparent to you, **no action is needed**. You can learn more in [issue #33566](https://github.com/microsoft/playwright/issues/33566).
|
||||
|
||||
If you are only running tests in headless, for example on CI, you can avoid downloading a headed version of Chromium by specifying `chromium-headless-shell` during installation.
|
||||
|
||||
```bash
|
||||
# only running tests headlessly
|
||||
npx playwright install chromium-headless-shell firefox webkit
|
||||
```
|
||||
|
||||
Playwright will skip downloading headed chromium build, and will use `chromium-headless-shell` when running headless.
|
||||
|
||||
#### Opt-in to new headless
|
||||
|
||||
We encourage everyone to try and switch to the new headless by using the `chromium-next` channel.
|
||||
|
||||
First, install this channel prior to running tests. Make sure to list all the browsers that you use.
|
||||
|
||||
```bash
|
||||
npx playwright install chromium-next firefox webkit
|
||||
```
|
||||
|
||||
Then update your config file to specify `'chromium-next'` channel.
|
||||
|
||||
```js
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
channel: 'chromium-next',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
After updating to Playwright v1.49, run your test suite. If it still passes, you're good to go. If not, you will probably need to update your snapshots, and adapt some of your test code around PDF viewers and extensions. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for more details.
|
||||
|
||||
### Other breaking changes
|
||||
|
||||
|
|
@ -92,6 +54,27 @@ export default defineConfig({
|
|||
- Package `@playwright/experimental-ct-vue2` will no longer be updated.
|
||||
- Package `@playwright/experimental-ct-solid` will no longer be updated.
|
||||
|
||||
### Try new Chromium headless
|
||||
|
||||
You can opt into the new headless mode by using `'chromium'` channel. As [official Chrome documentation puts it](https://developer.chrome.com/blog/chrome-headless-shell):
|
||||
|
||||
> New Headless on the other hand is the real Chrome browser, and is thus more authentic, reliable, and offers more features. This makes it more suitable for high-accuracy end-to-end web app testing or browser extension testing.
|
||||
|
||||
See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for the list of possible breakages you could encounter and more details on Chromium headless. Please file an issue if you see any problems after opting in.
|
||||
|
||||
```js
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chromium' },
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- `<canvas>` elements inside a snapshot now draw a preview.
|
||||
|
|
|
|||
60
package-lock.json
generated
60
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "playwright-internal",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
|
@ -7733,10 +7733,10 @@
|
|||
"version": "0.0.0"
|
||||
},
|
||||
"packages/playwright": {
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.49.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7750,11 +7750,11 @@
|
|||
},
|
||||
"packages/playwright-browser-chromium": {
|
||||
"name": "@playwright/browser-chromium",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.49.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
@ -7762,11 +7762,11 @@
|
|||
},
|
||||
"packages/playwright-browser-firefox": {
|
||||
"name": "@playwright/browser-firefox",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.49.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
@ -7774,22 +7774,22 @@
|
|||
},
|
||||
"packages/playwright-browser-webkit": {
|
||||
"name": "@playwright/browser-webkit",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.49.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/playwright-chromium": {
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.49.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7799,7 +7799,7 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-core": {
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
|
|
@ -7810,11 +7810,11 @@
|
|||
},
|
||||
"packages/playwright-ct-core": {
|
||||
"name": "@playwright/experimental-ct-core",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.49.0-next",
|
||||
"playwright-core": "1.49.0-next",
|
||||
"playwright": "1.49.0",
|
||||
"playwright-core": "1.49.0",
|
||||
"vite": "^5.2.8"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -7823,10 +7823,10 @@
|
|||
},
|
||||
"packages/playwright-ct-react": {
|
||||
"name": "@playwright/experimental-ct-react",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"@playwright/experimental-ct-core": "1.49.0",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -7838,10 +7838,10 @@
|
|||
},
|
||||
"packages/playwright-ct-react17": {
|
||||
"name": "@playwright/experimental-ct-react17",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"@playwright/experimental-ct-core": "1.49.0",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -7853,10 +7853,10 @@
|
|||
},
|
||||
"packages/playwright-ct-svelte": {
|
||||
"name": "@playwright/experimental-ct-svelte",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"@playwright/experimental-ct-core": "1.49.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -7871,10 +7871,10 @@
|
|||
},
|
||||
"packages/playwright-ct-vue": {
|
||||
"name": "@playwright/experimental-ct-vue",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"@playwright/experimental-ct-core": "1.49.0",
|
||||
"@vitejs/plugin-vue": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -7885,11 +7885,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-firefox": {
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.49.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7900,10 +7900,10 @@
|
|||
},
|
||||
"packages/playwright-test": {
|
||||
"name": "@playwright/test",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.49.0-next"
|
||||
"playwright": "1.49.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7913,11 +7913,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-webkit": {
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.49.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"private": true,
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -24,4 +24,4 @@ try {
|
|||
}
|
||||
|
||||
if (install)
|
||||
install(['chromium', 'ffmpeg']);
|
||||
install(['chromium', 'chromium-headless-shell', 'ffmpeg']);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/browser-chromium",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "Playwright package that automatically installs Chromium",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -27,6 +27,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.49.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/browser-firefox",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "Playwright package that automatically installs Firefox",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -27,6 +27,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.49.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/browser-webkit",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "Playwright package that automatically installs WebKit",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -27,6 +27,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.49.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,4 +24,4 @@ try {
|
|||
}
|
||||
|
||||
if (install)
|
||||
install(['chromium', 'ffmpeg']);
|
||||
install(['chromium', 'chromium-headless-shell', 'ffmpeg']);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-chromium",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "A high-level API to automate Chromium",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,6 +30,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.49.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-core",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -96,16 +96,42 @@ function suggestedBrowsersToInstall() {
|
|||
return registry.executables().filter(e => e.installType !== 'none' && e.type !== 'tool').map(e => e.name).join(', ');
|
||||
}
|
||||
|
||||
function checkBrowsersToInstall(args: string[]): Executable[] {
|
||||
function defaultBrowsersToInstall(options: { noShell?: boolean, onlyShell?: boolean }): Executable[] {
|
||||
let executables = registry.defaultExecutables();
|
||||
if (options.noShell)
|
||||
executables = executables.filter(e => e.name !== 'chromium-headless-shell');
|
||||
if (options.onlyShell)
|
||||
executables = executables.filter(e => e.name !== 'chromium');
|
||||
return executables;
|
||||
}
|
||||
|
||||
function checkBrowsersToInstall(args: string[], options: { noShell?: boolean, onlyShell?: boolean }): Executable[] {
|
||||
if (options.noShell && options.onlyShell)
|
||||
throw new Error(`Only one of --no-shell and --only-shell can be specified`);
|
||||
|
||||
const faultyArguments: string[] = [];
|
||||
const executables: Executable[] = [];
|
||||
for (const arg of args) {
|
||||
const handleArgument = (arg: string) => {
|
||||
const executable = registry.findExecutable(arg);
|
||||
if (!executable || executable.installType === 'none')
|
||||
faultyArguments.push(arg);
|
||||
else
|
||||
executables.push(executable);
|
||||
if (executable?.browserName === 'chromium')
|
||||
executables.push(registry.findExecutable('ffmpeg')!);
|
||||
};
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg === 'chromium') {
|
||||
if (!options.onlyShell)
|
||||
handleArgument('chromium');
|
||||
if (!options.noShell)
|
||||
handleArgument('chromium-headless-shell');
|
||||
} else {
|
||||
handleArgument(arg);
|
||||
}
|
||||
}
|
||||
|
||||
if (faultyArguments.length)
|
||||
throw new Error(`Invalid installation targets: ${faultyArguments.map(name => `'${name}'`).join(', ')}. Expecting one of: ${suggestedBrowsersToInstall()}`);
|
||||
return executables;
|
||||
|
|
@ -118,7 +144,12 @@ program
|
|||
.option('--with-deps', 'install system dependencies for browsers')
|
||||
.option('--dry-run', 'do not execute installation, only print information')
|
||||
.option('--force', 'force reinstall of stable browser channels')
|
||||
.action(async function(args: string[], options: { withDeps?: boolean, force?: boolean, dryRun?: boolean }) {
|
||||
.option('--only-shell', 'only install headless shell when installing chromium')
|
||||
.option('--no-shell', 'do not install chromium headless shell')
|
||||
.action(async function(args: string[], options: { withDeps?: boolean, force?: boolean, dryRun?: boolean, shell?: boolean, noShell?: boolean, onlyShell?: boolean }) {
|
||||
// For '--no-shell' option, commander sets `shell: false` instead.
|
||||
if (options.shell === false)
|
||||
options.noShell = true;
|
||||
if (isLikelyNpxGlobal()) {
|
||||
console.error(wrapInASCIIBox([
|
||||
`WARNING: It looks like you are running 'npx playwright install' without first`,
|
||||
|
|
@ -141,7 +172,7 @@ program
|
|||
}
|
||||
try {
|
||||
const hasNoArguments = !args.length;
|
||||
const executables = hasNoArguments ? registry.defaultExecutables() : checkBrowsersToInstall(args);
|
||||
const executables = hasNoArguments ? defaultBrowsersToInstall(options) : checkBrowsersToInstall(args, options);
|
||||
if (options.withDeps)
|
||||
await registry.installDeps(executables, !!options.dryRun);
|
||||
if (options.dryRun) {
|
||||
|
|
@ -199,9 +230,9 @@ program
|
|||
.action(async function(args: string[], options: { dryRun?: boolean }) {
|
||||
try {
|
||||
if (!args.length)
|
||||
await registry.installDeps(registry.defaultExecutables(), !!options.dryRun);
|
||||
await registry.installDeps(defaultBrowsersToInstall({}), !!options.dryRun);
|
||||
else
|
||||
await registry.installDeps(checkBrowsersToInstall(args), !!options.dryRun);
|
||||
await registry.installDeps(checkBrowsersToInstall(args, {}), !!options.dryRun);
|
||||
} catch (e) {
|
||||
console.log(`Failed to install browser dependencies\n${e}`);
|
||||
gracefullyProcessExitDoNotHang(1);
|
||||
|
|
|
|||
|
|
@ -422,7 +422,8 @@ scheme.DebugControllerSetRecorderModeParams = tObject({
|
|||
});
|
||||
scheme.DebugControllerSetRecorderModeResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerHighlightParams = tObject({
|
||||
selector: tString,
|
||||
selector: tOptional(tString),
|
||||
ariaTemplate: tOptional(tString),
|
||||
});
|
||||
scheme.DebugControllerHighlightResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerHideHighlightParams = tOptional(tObject({}));
|
||||
|
|
|
|||
|
|
@ -15,12 +15,16 @@
|
|||
*/
|
||||
|
||||
import { parseYamlTemplate } from '../utils/isomorphic/ariaSnapshot';
|
||||
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot';
|
||||
import type { AriaTemplateNode, ParsedYaml } from '@isomorphic/ariaSnapshot';
|
||||
import { yaml } from '../utilsBundle';
|
||||
|
||||
export function parseAriaSnapshot(text: string): AriaTemplateNode {
|
||||
const fragment = yaml.parse(text);
|
||||
if (!Array.isArray(fragment))
|
||||
throw new Error('Expected object key starting with "- ":\n\n' + text + '\n');
|
||||
return parseYamlTemplate(fragment);
|
||||
return parseYamlTemplate(parseYamlForAriaSnapshot(text));
|
||||
}
|
||||
|
||||
export function parseYamlForAriaSnapshot(text: string): ParsedYaml {
|
||||
const parsed = yaml.parse(text);
|
||||
if (!Array.isArray(parsed))
|
||||
throw new Error('Expected object key starting with "- ":\n\n' + text + '\n');
|
||||
return parsed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import type { Playwright } from './playwright';
|
|||
import { Recorder } from './recorder';
|
||||
import { EmptyRecorderApp } from './recorder/recorderApp';
|
||||
import { asLocator, type Language } from '../utils';
|
||||
import { parseYamlForAriaSnapshot } from './ariaSnapshot';
|
||||
|
||||
const internalMetadata = serverSideCallMetadata();
|
||||
|
||||
|
|
@ -142,9 +143,13 @@ export class DebugController extends SdkObject {
|
|||
this._autoCloseTimer = setTimeout(heartBeat, 30000);
|
||||
}
|
||||
|
||||
async highlight(selector: string) {
|
||||
for (const recorder of await this._allRecorders())
|
||||
recorder.setHighlightedSelector(this._sdkLanguage, selector);
|
||||
async highlight(params: { selector?: string, ariaTemplate?: string }) {
|
||||
for (const recorder of await this._allRecorders()) {
|
||||
if (params.ariaTemplate)
|
||||
recorder.setHighlightedAriaTemplate(parseYamlForAriaSnapshot(params.ariaTemplate));
|
||||
else if (params.selector)
|
||||
recorder.setHighlightedSelector(this._sdkLanguage, params.selector);
|
||||
}
|
||||
}
|
||||
|
||||
async hideHighlight() {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export class DebugControllerDispatcher extends Dispatcher<DebugController, chann
|
|||
}
|
||||
|
||||
async highlight(params: channels.DebugControllerHighlightParams) {
|
||||
await this._object.highlight(params.selector);
|
||||
await this._object.highlight(params);
|
||||
}
|
||||
|
||||
async hideHighlight() {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ export {
|
|||
registry,
|
||||
registryDirectory,
|
||||
Registry,
|
||||
installDefaultBrowsersForNpmInstall,
|
||||
installBrowsersForNpmInstall,
|
||||
writeDockerVersion } from './registry';
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,8 @@ export class Highlight {
|
|||
}
|
||||
|
||||
install() {
|
||||
if (!this._injectedScript.document.documentElement.contains(this._glassPaneElement))
|
||||
// NOTE: document.documentElement can be null: https://github.com/microsoft/TypeScript/issues/50078
|
||||
if (this._injectedScript.document.documentElement && !this._injectedScript.document.documentElement.contains(this._glassPaneElement))
|
||||
this._injectedScript.document.documentElement.appendChild(this._glassPaneElement);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -492,9 +492,10 @@ class RecordActionTool implements RecorderTool {
|
|||
return;
|
||||
const result = activeElement ? this._recorder.injectedScript.generateSelector(activeElement, { testIdAttributeName: this._recorder.state.testIdAttributeName }) : null;
|
||||
this._activeModel = result && result.selector ? result : null;
|
||||
if (userGesture)
|
||||
if (userGesture) {
|
||||
this._hoveredElement = activeElement as HTMLElement | null;
|
||||
this._updateModelForHoveredElement();
|
||||
this._updateModelForHoveredElement();
|
||||
}
|
||||
}
|
||||
|
||||
private _shouldIgnoreMouseEvent(event: MouseEvent): boolean {
|
||||
|
|
@ -589,6 +590,8 @@ class RecordActionTool implements RecorderTool {
|
|||
}
|
||||
|
||||
private _updateModelForHoveredElement() {
|
||||
if (this._performingActions.size)
|
||||
return;
|
||||
if (!this._hoveredElement || !this._hoveredElement.isConnected) {
|
||||
this._hoveredModel = null;
|
||||
this._hoveredElement = null;
|
||||
|
|
@ -1018,7 +1021,7 @@ export class Recorder {
|
|||
private _listeners: (() => void)[] = [];
|
||||
private _currentTool: RecorderTool;
|
||||
private _tools: Record<Mode, RecorderTool>;
|
||||
private _actionSelectorModel: HighlightModel | null = null;
|
||||
private _lastHighlightedSelector: string | undefined = undefined;
|
||||
private _lastHighlightedAriaTemplateJSON: string = 'undefined';
|
||||
readonly highlight: Highlight;
|
||||
readonly overlay: Overlay | undefined;
|
||||
|
|
@ -1129,12 +1132,12 @@ export class Recorder {
|
|||
this._switchCurrentTool();
|
||||
this.overlay?.setUIState(state);
|
||||
|
||||
// Race or scroll.
|
||||
if (this._actionSelectorModel?.selector && !this._actionSelectorModel?.elements.length && !this._lastHighlightedAriaTemplateJSON)
|
||||
this._actionSelectorModel = null;
|
||||
|
||||
if (state.actionSelector && state.actionSelector !== this._actionSelectorModel?.selector)
|
||||
this._actionSelectorModel = querySelector(this.injectedScript, state.actionSelector, this.document);
|
||||
let highlight: HighlightModel | 'clear' | 'noop' = 'noop';
|
||||
if (state.actionSelector !== this._lastHighlightedSelector) {
|
||||
this._lastHighlightedSelector = state.actionSelector;
|
||||
const model = state.actionSelector ? querySelector(this.injectedScript, state.actionSelector, this.document) : null;
|
||||
highlight = model?.elements.length ? model : 'clear';
|
||||
}
|
||||
|
||||
const ariaTemplateJSON = JSON.stringify(state.ariaTemplate);
|
||||
if (this._lastHighlightedAriaTemplateJSON !== ariaTemplateJSON) {
|
||||
|
|
@ -1142,16 +1145,15 @@ export class Recorder {
|
|||
const template = state.ariaTemplate ? this.injectedScript.utils.parseYamlTemplate(state.ariaTemplate) : undefined;
|
||||
const elements = template ? this.injectedScript.getAllByAria(this.document, template) : [];
|
||||
if (elements.length)
|
||||
this._actionSelectorModel = { elements };
|
||||
highlight = { elements };
|
||||
else
|
||||
this._actionSelectorModel = null;
|
||||
highlight = 'clear';
|
||||
}
|
||||
|
||||
if (!state.actionSelector && !state.ariaTemplate)
|
||||
this._actionSelectorModel = null;
|
||||
|
||||
if (this.state.mode === 'none' || this.state.mode === 'standby')
|
||||
this.updateHighlight(this._actionSelectorModel, false);
|
||||
if (highlight === 'clear')
|
||||
this.clearHighlight();
|
||||
else if (highlight !== 'noop')
|
||||
this.updateHighlight(highlight, false);
|
||||
}
|
||||
|
||||
clearHighlight() {
|
||||
|
|
@ -1266,6 +1268,8 @@ export class Recorder {
|
|||
private _onScroll(event: Event) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
this._lastHighlightedSelector = undefined;
|
||||
this._lastHighlightedAriaTemplateJSON = 'undefined';
|
||||
this.highlight.hideActionPoint();
|
||||
this._currentTool.onScroll?.(event);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
|
|||
readonly handleSIGINT: boolean | undefined;
|
||||
private _context: BrowserContext;
|
||||
private _mode: Mode;
|
||||
private _highlightedElement: { selector?: string, ariaSnapshot?: ParsedYaml } = {};
|
||||
private _highlightedElement: { selector?: string, ariaTemplate?: ParsedYaml } = {};
|
||||
private _overlayState: OverlayState = { offsetX: 0 };
|
||||
private _recorderApp: IRecorderApp | null = null;
|
||||
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
|
||||
|
|
@ -107,8 +107,8 @@ export class Recorder implements InstrumentationListener, IRecorder {
|
|||
if (data.event === 'highlightRequested') {
|
||||
if (data.params.selector)
|
||||
this.setHighlightedSelector(this._currentLanguage, data.params.selector);
|
||||
if (data.params.ariaSnapshot)
|
||||
this.setHighlightedAriaSnapshot(data.params.ariaSnapshot);
|
||||
if (data.params.ariaTemplate)
|
||||
this.setHighlightedAriaTemplate(data.params.ariaTemplate);
|
||||
return;
|
||||
}
|
||||
if (data.event === 'step') {
|
||||
|
|
@ -169,7 +169,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
|
|||
mode: this._mode,
|
||||
actionPoint,
|
||||
actionSelector,
|
||||
ariaTemplate: this._highlightedElement.ariaSnapshot,
|
||||
ariaTemplate: this._highlightedElement.ariaTemplate,
|
||||
language: this._currentLanguage,
|
||||
testIdAttributeName: this._contextRecorder.testIdAttributeName(),
|
||||
overlay: this._overlayState,
|
||||
|
|
@ -245,8 +245,8 @@ export class Recorder implements InstrumentationListener, IRecorder {
|
|||
this._refreshOverlay();
|
||||
}
|
||||
|
||||
setHighlightedAriaSnapshot(ariaSnapshot: ParsedYaml) {
|
||||
this._highlightedElement = { ariaSnapshot };
|
||||
setHighlightedAriaTemplate(ariaTemplate: ParsedYaml) {
|
||||
this._highlightedElement = { ariaTemplate };
|
||||
this._refreshOverlay();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ const EXECUTABLE_PATHS = {
|
|||
};
|
||||
|
||||
type DownloadPaths = Record<HostPlatform, string | undefined>;
|
||||
const DOWNLOAD_PATHS: Record<BrowserName | InternalTool | 'chromium-headless-shell', DownloadPaths> = {
|
||||
const DOWNLOAD_PATHS: Record<BrowserName | InternalTool, DownloadPaths> = {
|
||||
'chromium': {
|
||||
'<unknown>': undefined,
|
||||
'ubuntu18.04-x64': undefined,
|
||||
|
|
@ -403,9 +403,9 @@ function readDescriptors(browsersJSON: BrowsersJSON): BrowsersJSONDescriptor[] {
|
|||
}
|
||||
|
||||
export type BrowserName = 'chromium' | 'firefox' | 'webkit' | 'bidi';
|
||||
type InternalTool = 'ffmpeg' | 'firefox-beta' | 'chromium-tip-of-tree' | 'android';
|
||||
type InternalTool = 'ffmpeg' | 'firefox-beta' | 'chromium-tip-of-tree' | 'chromium-headless-shell' | 'android';
|
||||
type BidiChannel = 'bidi-firefox-stable' | 'bidi-firefox-beta' | 'bidi-firefox-nightly' | 'bidi-chrome-canary' | 'bidi-chrome-stable' | 'bidi-chromium';
|
||||
type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'chromium-headless-shell' | 'chromium-next' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary';
|
||||
type ChromiumChannel = 'chrome' | 'chrome-beta' | 'chrome-dev' | 'chrome-canary' | 'msedge' | 'msedge-beta' | 'msedge-dev' | 'msedge-canary';
|
||||
const allDownloadable = ['android', 'chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree', 'chromium-headless-shell'];
|
||||
|
||||
export interface Executable {
|
||||
|
|
@ -488,21 +488,6 @@ export class Registry {
|
|||
_dependencyGroup: 'chromium',
|
||||
_isHermeticInstallation: true,
|
||||
});
|
||||
this._executables.push({
|
||||
type: 'channel',
|
||||
name: 'chromium-next',
|
||||
browserName: 'chromium',
|
||||
directory: chromium.dir,
|
||||
executablePath: () => chromiumExecutable,
|
||||
executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium-next', chromiumExecutable, chromium.installByDefault, sdkLanguage),
|
||||
installType: 'download-on-demand',
|
||||
_validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, chromium.dir, ['chrome-linux'], [], ['chrome-win']),
|
||||
downloadURLs: this._downloadURLs(chromium),
|
||||
browserVersion: chromium.browserVersion,
|
||||
_install: () => this._downloadExecutable(chromium, chromiumExecutable),
|
||||
_dependencyGroup: 'chromium',
|
||||
_isHermeticInstallation: true,
|
||||
});
|
||||
|
||||
const chromiumHeadlessShell = descriptors.find(d => d.name === 'chromium-headless-shell')!;
|
||||
const chromiumHeadlessShellExecutable = findExecutablePath(chromiumHeadlessShell.dir, 'chromium-headless-shell');
|
||||
|
|
@ -512,7 +497,7 @@ export class Registry {
|
|||
browserName: 'chromium',
|
||||
directory: chromiumHeadlessShell.dir,
|
||||
executablePath: () => chromiumHeadlessShellExecutable,
|
||||
executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium-headless-shell', chromiumHeadlessShellExecutable, false, sdkLanguage),
|
||||
executablePathOrDie: (sdkLanguage: string) => executablePathOrDie('chromium', chromiumHeadlessShellExecutable, chromiumHeadlessShell.installByDefault, sdkLanguage),
|
||||
installType: chromiumHeadlessShell.installByDefault ? 'download-by-default' : 'download-on-demand',
|
||||
_validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, chromiumHeadlessShell.dir, ['chrome-linux'], [], ['chrome-win']),
|
||||
downloadURLs: this._downloadURLs(chromiumHeadlessShell),
|
||||
|
|
@ -894,16 +879,8 @@ export class Registry {
|
|||
return this._executables.filter(e => e.installType === 'download-by-default');
|
||||
}
|
||||
|
||||
private _addRequirementsAndDedupe(executables: Executable[]): ExecutableImpl[] {
|
||||
const set = new Set<ExecutableImpl>();
|
||||
for (const executable of executables as ExecutableImpl[]) {
|
||||
set.add(executable);
|
||||
if (executable.browserName === 'chromium')
|
||||
set.add(this.findExecutable('ffmpeg')!);
|
||||
if (executable.name === 'chromium')
|
||||
set.add(this.findExecutable('chromium-headless-shell')!);
|
||||
}
|
||||
return Array.from(set);
|
||||
private _dedupe(executables: Executable[]): ExecutableImpl[] {
|
||||
return Array.from(new Set(executables as ExecutableImpl[]));
|
||||
}
|
||||
|
||||
private async _validateHostRequirements(sdkLanguage: string, browserDirectory: string, linuxLddDirectories: string[], dlOpenLibraries: string[], windowsExeAndDllDirectories: string[]) {
|
||||
|
|
@ -914,7 +891,7 @@ export class Registry {
|
|||
}
|
||||
|
||||
async installDeps(executablesToInstallDeps: Executable[], dryRun: boolean) {
|
||||
const executables = this._addRequirementsAndDedupe(executablesToInstallDeps);
|
||||
const executables = this._dedupe(executablesToInstallDeps);
|
||||
const targets = new Set<DependencyGroup>();
|
||||
for (const executable of executables) {
|
||||
if (executable._dependencyGroup)
|
||||
|
|
@ -928,7 +905,7 @@ export class Registry {
|
|||
}
|
||||
|
||||
async install(executablesToInstall: Executable[], forceReinstall: boolean) {
|
||||
const executables = this._addRequirementsAndDedupe(executablesToInstall);
|
||||
const executables = this._dedupe(executablesToInstall);
|
||||
await fs.promises.mkdir(registryDirectory, { recursive: true });
|
||||
const lockfilePath = path.join(registryDirectory, '__dirlock');
|
||||
const linksDir = path.join(registryDirectory, '.links');
|
||||
|
|
@ -1224,11 +1201,6 @@ export function buildPlaywrightCLICommand(sdkLanguage: string, parameters: strin
|
|||
}
|
||||
}
|
||||
|
||||
export async function installDefaultBrowsersForNpmInstall() {
|
||||
const defaultBrowserNames = registry.defaultExecutables().map(e => e.name);
|
||||
return installBrowsersForNpmInstall(defaultBrowserNames);
|
||||
}
|
||||
|
||||
export async function installBrowsersForNpmInstall(browsers: string[]) {
|
||||
// PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD should have a value of 0 or 1
|
||||
if (getAsBooleanFromENV('PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD')) {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
|
|||
const kTargetAttribute = '__playwright_target__';
|
||||
const kCustomElementsAttribute = '__playwright_custom_elements__';
|
||||
const kCurrentSrcAttribute = '__playwright_current_src__';
|
||||
const kBoundingRectAttribute = '__playwright_bounding_rect__';
|
||||
|
||||
// Symbols for our own info on Nodes/StyleSheets.
|
||||
const kSnapshotFrameId = Symbol('__playwright_snapshot_frameid_');
|
||||
|
|
@ -436,6 +437,18 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
|
|||
expectValue(value);
|
||||
attrs[kSelectedAttribute] = value;
|
||||
}
|
||||
if (nodeName === 'CANVAS') {
|
||||
const boundingRect = (element as HTMLCanvasElement).getBoundingClientRect();
|
||||
const value = JSON.stringify({
|
||||
left: boundingRect.left / window.innerWidth,
|
||||
top: boundingRect.top / window.innerHeight,
|
||||
right: boundingRect.right / window.innerWidth,
|
||||
bottom: boundingRect.bottom / window.innerHeight
|
||||
});
|
||||
expectValue(kBoundingRectAttribute);
|
||||
expectValue(value);
|
||||
attrs[kBoundingRectAttribute] = value;
|
||||
}
|
||||
if (element.scrollTop) {
|
||||
expectValue(kScrollTopAttribute);
|
||||
expectValue(element.scrollTop);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-core",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "Playwright Component Testing Helpers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -26,8 +26,8 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next",
|
||||
"playwright-core": "1.49.0",
|
||||
"vite": "^5.2.8",
|
||||
"playwright": "1.49.0-next"
|
||||
"playwright": "1.49.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-react",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "Playwright Component Testing for React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"@playwright/experimental-ct-core": "1.49.0",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-react17",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "Playwright Component Testing for React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"@playwright/experimental-ct-core": "1.49.0",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-svelte",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "Playwright Component Testing for Svelte",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"@playwright/experimental-ct-core": "1.49.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-vue",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "Playwright Component Testing for Vue",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"@playwright/experimental-ct-core": "1.49.0",
|
||||
"@vitejs/plugin-vue": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-firefox",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "A high-level API to automate Firefox",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,6 +30,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.49.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/test",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,6 +30,6 @@
|
|||
},
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"playwright": "1.49.0-next"
|
||||
"playwright": "1.49.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-webkit",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "A high-level API to automate WebKit",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,6 +30,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.49.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.49.0",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
},
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.49.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ export function toHaveAccessibleDescription(
|
|||
options?: { timeout?: number, ignoreCase?: boolean },
|
||||
) {
|
||||
return toMatchText.call(this, 'toHaveAccessibleDescription', locator, 'Locator', async (isNot, timeout) => {
|
||||
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase });
|
||||
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true });
|
||||
return await locator._expect('to.have.accessible.description', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
|
|
@ -193,7 +193,7 @@ export function toHaveAccessibleName(
|
|||
options?: { timeout?: number, ignoreCase?: boolean },
|
||||
) {
|
||||
return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => {
|
||||
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase });
|
||||
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true });
|
||||
return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import type { ReporterDescription } from '../../types/test';
|
||||
import type { FullConfigInternal } from '../common/config';
|
||||
import type { JsonConfig, JsonEvent, JsonFullResult, JsonLocation, JsonProject, JsonSuite, JsonTestCase, JsonTestResultEnd, JsonTestStepStart } from '../isomorphic/teleReceiver';
|
||||
import type { JsonConfig, JsonEvent, JsonFullResult, JsonLocation, JsonProject, JsonSuite, JsonTestCase, JsonTestResultEnd, JsonTestStepStart, JsonTestStepEnd } from '../isomorphic/teleReceiver';
|
||||
import { TeleReporterReceiver } from '../isomorphic/teleReceiver';
|
||||
import { JsonStringInternalizer, StringInternPool } from '../isomorphic/stringInternPool';
|
||||
import { createReporters } from '../runner/reporters';
|
||||
|
|
@ -471,7 +471,7 @@ class PathSeparatorPatcher {
|
|||
}
|
||||
if (jsonEvent.method === 'onTestEnd') {
|
||||
const testResult = jsonEvent.params.result as JsonTestResultEnd;
|
||||
testResult.errors.forEach(error => this._updateLocation(error.location));
|
||||
testResult.errors.forEach(error => this._updateErrorLocations(error));
|
||||
testResult.attachments.forEach(attachment => {
|
||||
if (attachment.path)
|
||||
attachment.path = this._updatePath(attachment.path);
|
||||
|
|
@ -483,6 +483,11 @@ class PathSeparatorPatcher {
|
|||
this._updateLocation(step.location);
|
||||
return;
|
||||
}
|
||||
if (jsonEvent.method === 'onStepEnd') {
|
||||
const step = jsonEvent.params.step as JsonTestStepEnd;
|
||||
this._updateErrorLocations(step.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private _updateProject(project: JsonProject) {
|
||||
|
|
@ -504,6 +509,13 @@ class PathSeparatorPatcher {
|
|||
}
|
||||
}
|
||||
|
||||
private _updateErrorLocations(error: TestError | undefined) {
|
||||
while (error) {
|
||||
this._updateLocation(error.location);
|
||||
error = error.cause;
|
||||
}
|
||||
}
|
||||
|
||||
private _updateLocation(location?: JsonLocation) {
|
||||
if (location)
|
||||
location.file = this._updatePath(location.file);
|
||||
|
|
|
|||
|
|
@ -83,6 +83,10 @@ export async function applySuggestedRebaselines(config: FullConfigInternal, repo
|
|||
const indent = lines[matcher.loc!.start.line - 1].match(/^\s*/)![0];
|
||||
const newText = replacement.code.replace(/\{indent\}/g, indent);
|
||||
ranges.push({ start: matcher.start!, end: node.end!, oldText: source.substring(matcher.start!, node.end!), newText });
|
||||
// We can have multiple, hopefully equal, replacements for the same location,
|
||||
// for example when a single test runs multiple times because of projects or retries.
|
||||
// Do not apply multiple replacements for the same assertion.
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -741,10 +741,12 @@ export type DebugControllerSetRecorderModeOptions = {
|
|||
};
|
||||
export type DebugControllerSetRecorderModeResult = void;
|
||||
export type DebugControllerHighlightParams = {
|
||||
selector: string,
|
||||
selector?: string,
|
||||
ariaTemplate?: string,
|
||||
};
|
||||
export type DebugControllerHighlightOptions = {
|
||||
|
||||
selector?: string,
|
||||
ariaTemplate?: string,
|
||||
};
|
||||
export type DebugControllerHighlightResult = void;
|
||||
export type DebugControllerHideHighlightParams = {};
|
||||
|
|
|
|||
|
|
@ -791,7 +791,8 @@ DebugController:
|
|||
|
||||
highlight:
|
||||
parameters:
|
||||
selector: string
|
||||
selector: string?
|
||||
ariaTemplate: string?
|
||||
|
||||
hideHighlight:
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { CallLogView } from './callLog';
|
|||
import './recorder.css';
|
||||
import { asLocator } from '@isomorphic/locatorGenerators';
|
||||
import { toggleTheme } from '@web/theme';
|
||||
import { copy } from '@web/uiUtils';
|
||||
import { copy, useSetting } from '@web/uiUtils';
|
||||
import yaml from 'yaml';
|
||||
import { parseAriaKey } from '@isomorphic/ariaSnapshot';
|
||||
import type { AriaKeyError, ParsedYaml } from '@isomorphic/ariaSnapshot';
|
||||
|
|
@ -47,7 +47,7 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||
}) => {
|
||||
const [selectedFileId, setSelectedFileId] = React.useState<string | undefined>();
|
||||
const [runningFileId, setRunningFileId] = React.useState<string | undefined>();
|
||||
const [selectedTab, setSelectedTab] = React.useState<string>('log');
|
||||
const [selectedTab, setSelectedTab] = useSetting<string>('recorderPropertiesTab', 'log');
|
||||
const [ariaSnapshot, setAriaSnapshot] = React.useState<string | undefined>();
|
||||
const [ariaSnapshotErrors, setAriaSnapshotErrors] = React.useState<SourceHighlight[]>();
|
||||
|
||||
|
|
@ -67,6 +67,7 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||
const language = source.language;
|
||||
setLocator(asLocator(language, elementInfo.selector));
|
||||
setAriaSnapshot(elementInfo.ariaSnapshot);
|
||||
setAriaSnapshotErrors([]);
|
||||
if (userGesture && selectedTab !== 'locator' && selectedTab !== 'aria')
|
||||
setSelectedTab('locator');
|
||||
|
||||
|
|
@ -120,11 +121,8 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||
setAriaSnapshotErrors(errors);
|
||||
setAriaSnapshot(ariaSnapshot);
|
||||
if (!errors.length)
|
||||
window.dispatch({ event: 'highlightRequested', params: { ariaSnapshot: fragment } });
|
||||
window.dispatch({ event: 'highlightRequested', params: { ariaTemplate: fragment } });
|
||||
}, [mode]);
|
||||
const isRecording = mode === 'recording' || mode === 'recording-inspecting';
|
||||
const locatorPlaceholder = isRecording ? '// Unavailable while recording' : (locator ? undefined : '// Pick element or type locator');
|
||||
const ariaPlaceholder = isRecording ? '# Unavailable while recording' : (ariaSnapshot ? undefined : '# Pick element or type snapshot');
|
||||
|
||||
return <div className='recorder'>
|
||||
<Toolbar>
|
||||
|
|
@ -191,7 +189,7 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||
{
|
||||
id: 'locator',
|
||||
title: 'Locator',
|
||||
render: () => <CodeMirrorWrapper text={locatorPlaceholder || locator} language={source.language} readOnly={isRecording} focusOnChange={true} onChange={onEditorChange} wrapLines={true} />
|
||||
render: () => <CodeMirrorWrapper text={locator} placeholder='Type locator to inspect' language={source.language} focusOnChange={true} onChange={onEditorChange} wrapLines={true} />
|
||||
},
|
||||
{
|
||||
id: 'log',
|
||||
|
|
@ -200,8 +198,8 @@ export const Recorder: React.FC<RecorderProps> = ({
|
|||
},
|
||||
{
|
||||
id: 'aria',
|
||||
title: 'Aria snapshot',
|
||||
render: () => <CodeMirrorWrapper text={ariaPlaceholder || ariaSnapshot || ''} language={'yaml'} readOnly={isRecording} onChange={onAriaEditorChange} highlight={ariaSnapshotErrors} wrapLines={true} />
|
||||
title: 'Aria',
|
||||
render: () => <CodeMirrorWrapper text={ariaSnapshot || ''} placeholder='Type aria template to match' language={'yaml'} onChange={onAriaEditorChange} highlight={ariaSnapshotErrors} wrapLines={true} />
|
||||
},
|
||||
]}
|
||||
selectedTab={selectedTab}
|
||||
|
|
|
|||
|
|
@ -427,14 +427,20 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
|
|||
for (const canvas of canvasElements) {
|
||||
const context = canvas.getContext('2d')!;
|
||||
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
const xStart = boundingRect.left / window.innerWidth;
|
||||
const yStart = boundingRect.top / window.innerHeight;
|
||||
const xEnd = boundingRect.right / window.innerWidth;
|
||||
const yEnd = boundingRect.bottom / window.innerHeight;
|
||||
const boundingRectAttribute = canvas.getAttribute('__playwright_bounding_rect__');
|
||||
canvas.removeAttribute('__playwright_bounding_rect__');
|
||||
if (!boundingRectAttribute)
|
||||
continue;
|
||||
|
||||
const partiallyUncaptured = xEnd > 1 || yEnd > 1;
|
||||
const fullyUncaptured = xStart > 1 || yStart > 1;
|
||||
let boundingRect: { left: number, top: number, right: number, bottom: number };
|
||||
try {
|
||||
boundingRect = JSON.parse(boundingRectAttribute);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const partiallyUncaptured = boundingRect.right > 1 || boundingRect.bottom > 1;
|
||||
const fullyUncaptured = boundingRect.left > 1 || boundingRect.top > 1;
|
||||
if (fullyUncaptured) {
|
||||
canvas.title = `Playwright couldn't capture canvas contents because it's located outside the viewport.`;
|
||||
continue;
|
||||
|
|
@ -442,10 +448,10 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
|
|||
|
||||
drawCheckerboard(context, canvas);
|
||||
|
||||
context.drawImage(img, xStart * img.width, yStart * img.height, (xEnd - xStart) * img.width, (yEnd - yStart) * img.height, 0, 0, canvas.width, canvas.height);
|
||||
context.drawImage(img, boundingRect.left * img.width, boundingRect.top * img.height, (boundingRect.right - boundingRect.left) * img.width, (boundingRect.bottom - boundingRect.top) * img.height, 0, 0, canvas.width, canvas.height);
|
||||
if (isUnderTest)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`canvas drawn:`, JSON.stringify([xStart, yStart, xEnd, yEnd].map(v => Math.floor(v * 100))));
|
||||
console.log(`canvas drawn:`, JSON.stringify([boundingRect.left, boundingRect.top, (boundingRect.right - boundingRect.left), (boundingRect.bottom - boundingRect.top)].map(v => Math.floor(v * 100))));
|
||||
|
||||
if (partiallyUncaptured)
|
||||
canvas.title = `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import 'codemirror/mode/javascript/javascript';
|
|||
import 'codemirror/mode/python/python';
|
||||
import 'codemirror/mode/clike/clike';
|
||||
import 'codemirror/mode/markdown/markdown';
|
||||
import 'codemirror/addon/display/placeholder';
|
||||
import 'codemirror/addon/mode/simple';
|
||||
import 'codemirror/mode/yaml/yaml';
|
||||
|
||||
|
|
|
|||
|
|
@ -181,3 +181,7 @@ body.dark-mode .CodeMirror span.cm-type {
|
|||
text-decoration-color: var(--vscode-errorForeground);
|
||||
text-decoration-style: wavy;
|
||||
}
|
||||
|
||||
.CodeMirror-placeholder {
|
||||
color: var(--vscode-input-placeholderForeground) !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export interface SourceProps {
|
|||
wrapLines?: boolean;
|
||||
onChange?: (text: string) => void;
|
||||
dataTestId?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
||||
|
|
@ -62,6 +63,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
|||
wrapLines,
|
||||
onChange,
|
||||
dataTestId,
|
||||
placeholder,
|
||||
}) => {
|
||||
const [measure, codemirrorElement] = useMeasure<HTMLDivElement>();
|
||||
const [modulePromise] = React.useState<Promise<CodeMirror>>(import('./codeMirrorModule').then(m => m.default));
|
||||
|
|
@ -89,7 +91,8 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
|||
&& mode === codemirrorRef.current.cm.getOption('mode')
|
||||
&& !!readOnly === codemirrorRef.current.cm.getOption('readOnly')
|
||||
&& lineNumbers === codemirrorRef.current.cm.getOption('lineNumbers')
|
||||
&& wrapLines === codemirrorRef.current.cm.getOption('lineWrapping')) {
|
||||
&& wrapLines === codemirrorRef.current.cm.getOption('lineWrapping')
|
||||
&& placeholder === codemirrorRef.current.cm.getOption('placeholder')) {
|
||||
// No need to re-create codemirror.
|
||||
return;
|
||||
}
|
||||
|
|
@ -102,6 +105,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
|||
readOnly: !!readOnly,
|
||||
lineNumbers,
|
||||
lineWrapping: wrapLines,
|
||||
placeholder,
|
||||
});
|
||||
codemirrorRef.current = { cm };
|
||||
if (isFocused)
|
||||
|
|
@ -109,7 +113,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
|||
setCodemirror(cm);
|
||||
return cm;
|
||||
})();
|
||||
}, [modulePromise, codemirror, codemirrorElement, language, mimeType, linkify, lineNumbers, wrapLines, readOnly, isFocused]);
|
||||
}, [modulePromise, codemirror, codemirrorElement, language, mimeType, linkify, lineNumbers, wrapLines, readOnly, isFocused, placeholder]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (codemirrorRef.current)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export type BrowserTestWorkerFixtures = PageWorkerFixtures & {
|
|||
browserType: BrowserType;
|
||||
isAndroid: boolean;
|
||||
isElectron: boolean;
|
||||
isHeadlessShell: boolean;
|
||||
nodeVersion: { major: number, minor: number, patch: number };
|
||||
bidiTestSkipPredicate: (info: TestInfo) => boolean;
|
||||
};
|
||||
|
|
@ -97,6 +98,10 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
|
|||
electronMajorVersion: [0, { scope: 'worker' }],
|
||||
isWebView2: [false, { scope: 'worker' }],
|
||||
|
||||
isHeadlessShell: [async ({ browserName, channel, headless }, use) => {
|
||||
await use(browserName === 'chromium' && (channel === 'chromium-headless-shell' || (!channel && headless)));
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
contextFactory: async ({ _contextFactory }: any, run) => {
|
||||
await run(_contextFactory);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -76,15 +76,24 @@ test(`playwright should work`, async ({ exec, installedSoftwareOnDisk }) => {
|
|||
await exec('node esm-playwright.mjs');
|
||||
});
|
||||
|
||||
test(`playwright should work with chromium-next`, async ({ exec, installedSoftwareOnDisk }) => {
|
||||
test(`playwright should work with chromium --no-shell`, async ({ exec, installedSoftwareOnDisk }) => {
|
||||
const result1 = await exec('npm i --foreground-scripts playwright');
|
||||
expect(result1).toHaveLoggedSoftwareDownload([]);
|
||||
expect(await installedSoftwareOnDisk()).toEqual([]);
|
||||
const result2 = await exec('npx playwright install chromium-next');
|
||||
const result2 = await exec('npx playwright install chromium --no-shell');
|
||||
expect(result2).toHaveLoggedSoftwareDownload(['chromium', 'ffmpeg']);
|
||||
expect(await installedSoftwareOnDisk()).toEqual(['chromium', 'ffmpeg']);
|
||||
});
|
||||
|
||||
test(`playwright should work with chromium --only-shell`, async ({ exec, installedSoftwareOnDisk }) => {
|
||||
const result1 = await exec('npm i --foreground-scripts playwright');
|
||||
expect(result1).toHaveLoggedSoftwareDownload([]);
|
||||
expect(await installedSoftwareOnDisk()).toEqual([]);
|
||||
const result2 = await exec('npx playwright install --only-shell');
|
||||
expect(result2).toHaveLoggedSoftwareDownload(['chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
||||
expect(await installedSoftwareOnDisk()).toEqual(['chromium-headless-shell', 'ffmpeg', 'firefox', 'webkit']);
|
||||
});
|
||||
|
||||
test('@playwright/test should work', async ({ exec, installedSoftwareOnDisk }) => {
|
||||
const result1 = await exec('npm i --foreground-scripts @playwright/test');
|
||||
expect(result1).toHaveLoggedSoftwareDownload([]);
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@
|
|||
import { browserTest as base, expect } from '../config/browserTest';
|
||||
|
||||
const it = base.extend<{ failsOn401: boolean }>({
|
||||
failsOn401: async ({ browserName, headless, channel }, use) => {
|
||||
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
failsOn401: async ({ browserName, isHeadlessShell }, use) => {
|
||||
await use(browserName === 'chromium' && !isHeadlessShell);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -52,8 +52,7 @@ it('should open devtools when "devtools: true" option is given', async ({ browse
|
|||
await browser.close();
|
||||
});
|
||||
|
||||
it('should return background pages', async ({ browserType, createUserDataDir, asset, headless, channel }) => {
|
||||
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
it('should return background pages', async ({ browserType, createUserDataDir, asset, isHeadlessShell }) => {
|
||||
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
|
||||
|
||||
const userDataDir = await createUserDataDir();
|
||||
|
|
@ -78,8 +77,7 @@ it('should return background pages', async ({ browserType, createUserDataDir, as
|
|||
expect(context.backgroundPages().length).toBe(0);
|
||||
});
|
||||
|
||||
it('should return background pages when recording video', async ({ browserType, createUserDataDir, asset, headless, channel }, testInfo) => {
|
||||
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
it('should return background pages when recording video', async ({ browserType, createUserDataDir, asset, isHeadlessShell }, testInfo) => {
|
||||
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
|
||||
|
||||
const userDataDir = await createUserDataDir();
|
||||
|
|
@ -105,8 +103,7 @@ it('should return background pages when recording video', async ({ browserType,
|
|||
await context.close();
|
||||
});
|
||||
|
||||
it('should support request/response events when using backgroundPage()', async ({ browserType, createUserDataDir, asset, server, headless, channel }) => {
|
||||
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
it('should support request/response events when using backgroundPage()', async ({ browserType, createUserDataDir, asset, server, isHeadlessShell }) => {
|
||||
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
|
||||
|
||||
server.setRoute('/empty.html', (req, res) => {
|
||||
|
|
@ -157,8 +154,7 @@ it('should support request/response events when using backgroundPage()', async (
|
|||
|
||||
it('should report console messages from content script', {
|
||||
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32762' }
|
||||
}, async ({ browserType, createUserDataDir, asset, server, headless, channel }) => {
|
||||
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
}, async ({ browserType, createUserDataDir, asset, server, isHeadlessShell }) => {
|
||||
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
|
||||
|
||||
const userDataDir = await createUserDataDir();
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { createGuid } from '../../packages/playwright-core/lib/utils/crypto';
|
|||
import { Backend } from '../config/debugControllerBackend';
|
||||
import type { Browser, BrowserContext } from '@playwright/test';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import { roundBox } from '../page/pageTest';
|
||||
|
||||
type BrowserWithReuse = Browser & { _newContextForReuse: () => Promise<BrowserContext> };
|
||||
type Fixtures = {
|
||||
|
|
@ -30,8 +31,9 @@ type Fixtures = {
|
|||
};
|
||||
|
||||
const test = baseTest.extend<Fixtures>({
|
||||
wsEndpoint: async ({ }, use) => {
|
||||
process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1';
|
||||
wsEndpoint: async ({ headless }, use) => {
|
||||
if (headless)
|
||||
process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1';
|
||||
const server = new PlaywrightServer({ mode: 'extension', path: '/' + createGuid(), maxConnections: Number.MAX_VALUE, enableSocksProxy: false });
|
||||
const wsEndpoint = await server.listen();
|
||||
await use(wsEndpoint);
|
||||
|
|
@ -279,3 +281,20 @@ test('should highlight inside iframe', async ({ backend, connectedBrowser }, tes
|
|||
await expect(highlight).toHaveCount(1);
|
||||
await expect(page.locator('x-pw-highlight')).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should highlight aria template', async ({ backend, connectedBrowser }, testInfo) => {
|
||||
const context = await connectedBrowser._newContextForReuse();
|
||||
const page = await context.newPage();
|
||||
await backend.navigate({ url: `data:text/html,<button>Submit</button>` });
|
||||
|
||||
const button = page.getByRole('button');
|
||||
const highlight = page.locator('x-pw-highlight');
|
||||
|
||||
await backend.highlight({ ariaTemplate: `- button "Submit2"` });
|
||||
await expect(highlight).toHaveCount(0);
|
||||
|
||||
await backend.highlight({ ariaTemplate: `- button "Submit"` });
|
||||
const box1 = roundBox(await button.boundingBox());
|
||||
const box2 = roundBox(await highlight.boundingBox());
|
||||
expect(box1).toEqual(box2);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -636,8 +636,7 @@ it('should be able to download a inline PDF file via response interception', asy
|
|||
await page.close();
|
||||
});
|
||||
|
||||
it('should be able to download a inline PDF file via navigation', async ({ browser, server, asset, browserName, channel, headless }) => {
|
||||
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
it('should be able to download a inline PDF file via navigation', async ({ browser, server, asset, browserName, isHeadlessShell }) => {
|
||||
it.skip(browserName === 'chromium' && !isHeadlessShell, 'We expect PDF Viewer to open up in headed Chromium');
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
|
|
|||
|
|
@ -101,10 +101,9 @@ it('should change document.activeElement', async ({ page, server }) => {
|
|||
expect(active).toEqual(['INPUT', 'TEXTAREA']);
|
||||
});
|
||||
|
||||
it('should not affect screenshots', async ({ page, server, browserName, headless, isWindows, channel }) => {
|
||||
it('should not affect screenshots', async ({ page, server, browserName, headless, isWindows, isHeadlessShell }) => {
|
||||
it.skip(browserName === 'webkit' && isWindows && !headless, 'WebKit/Windows/headed has a larger minimal viewport. See https://github.com/microsoft/playwright/issues/22616');
|
||||
it.skip(browserName === 'firefox' && !headless, 'Firefox headed produces a different image');
|
||||
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
it.fixme(browserName === 'chromium' && !isHeadlessShell, 'https://github.com/microsoft/playwright/issues/33330');
|
||||
|
||||
const page2 = await page.context().newPage();
|
||||
|
|
|
|||
|
|
@ -402,17 +402,6 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
await expect.poll(() => messages).toEqual(['mousedown', 'mouseup', 'click']);
|
||||
});
|
||||
|
||||
test('should update hover model on action', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name='updated'"></input>`);
|
||||
const [models] = await Promise.all([
|
||||
recorder.waitForActionPerformed(),
|
||||
page.click('input')
|
||||
]);
|
||||
expect(models.hovered).toBe('#checkbox');
|
||||
});
|
||||
|
||||
test('should reset hover model on action when element detaches', async ({ openRecorder }) => {
|
||||
const { page, recorder } = await openRecorder();
|
||||
|
||||
|
|
|
|||
|
|
@ -64,11 +64,10 @@ test.describe(() => {
|
|||
test('should inspect aria snapshot', async ({ openRecorder }) => {
|
||||
const { recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`<main><button>Submit</button></main>`);
|
||||
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
|
||||
await recorder.page.click('x-pw-tool-item.pick-locator');
|
||||
await recorder.page.hover('button');
|
||||
await recorder.trustedClick();
|
||||
await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click();
|
||||
await recorder.recorderPage.getByRole('tab', { name: 'Aria' }).click();
|
||||
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
|
||||
- textbox
|
||||
- text: '- button "Submit"'
|
||||
|
|
@ -85,12 +84,11 @@ test.describe(() => {
|
|||
const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
|
||||
const cancelButton = recorder.page.getByRole('button', { name: 'Cancel' });
|
||||
|
||||
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
|
||||
|
||||
await recorder.page.click('x-pw-tool-item.pick-locator');
|
||||
await submitButton.hover();
|
||||
await recorder.trustedClick();
|
||||
await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click();
|
||||
await recorder.recorderPage.getByRole('tab', { name: 'Aria' }).click();
|
||||
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
|
||||
- text: '- button "Submit"'
|
||||
`);
|
||||
|
|
@ -128,13 +126,12 @@ test.describe(() => {
|
|||
</main>`);
|
||||
|
||||
const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
|
||||
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
|
||||
|
||||
await recorder.page.click('x-pw-tool-item.pick-locator');
|
||||
await submitButton.hover();
|
||||
await recorder.trustedClick();
|
||||
|
||||
await recorder.recorderPage.getByRole('tab', { name: 'Aria snapshot ' }).click();
|
||||
await recorder.recorderPage.getByRole('tab', { name: 'Aria' }).click();
|
||||
await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
|
||||
- text: '- button "Submit"'
|
||||
`);
|
||||
|
|
|
|||
69
tests/library/inspector/cli-codegen-pick-locator.spec.ts
Normal file
69
tests/library/inspector/cli-codegen-pick-locator.spec.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { test, expect } from './inspectorTest';
|
||||
import { roundBox } from '../../page/pageTest';
|
||||
|
||||
test.describe(() => {
|
||||
test.skip(({ mode }) => mode !== 'default');
|
||||
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
|
||||
|
||||
test('should inspect locator', async ({ openRecorder }) => {
|
||||
const { recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`<main><button>Submit</button></main>`);
|
||||
await recorder.page.click('x-pw-tool-item.pick-locator');
|
||||
await recorder.page.hover('button');
|
||||
await recorder.trustedClick();
|
||||
await recorder.recorderPage.getByRole('tab', { name: 'Locator' }).click();
|
||||
await expect(recorder.recorderPage.locator('.tab-locator .CodeMirror')).toMatchAriaSnapshot(`
|
||||
- text: "getByRole('button', { name: 'Submit' })"
|
||||
`);
|
||||
});
|
||||
|
||||
test('should update locator highlight', async ({ openRecorder }) => {
|
||||
const { recorder } = await openRecorder();
|
||||
await recorder.setContentAndWait(`<main>
|
||||
<button>Submit</button>
|
||||
<button>Cancel</button>
|
||||
</main>`);
|
||||
|
||||
const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
|
||||
const cancelButton = recorder.page.getByRole('button', { name: 'Cancel' });
|
||||
|
||||
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
|
||||
|
||||
await recorder.page.click('x-pw-tool-item.pick-locator');
|
||||
await submitButton.hover();
|
||||
await recorder.trustedClick();
|
||||
await recorder.recorderPage.getByRole('tab', { name: 'Locator' }).click();
|
||||
await expect(recorder.recorderPage.locator('.tab-locator .CodeMirror')).toMatchAriaSnapshot(`
|
||||
- text: "getByRole('button', { name: 'Submit' })"
|
||||
`);
|
||||
|
||||
await recorder.recorderPage.locator('.tab-locator .CodeMirror').click();
|
||||
for (let i = 0; i < `Submit' })`.length; i++)
|
||||
await recorder.recorderPage.keyboard.press('Backspace');
|
||||
|
||||
{
|
||||
// Different button.
|
||||
await recorder.recorderPage.locator('.tab-locator .CodeMirror').pressSequentially(`Cancel' })`);
|
||||
await expect(recorder.page.locator('x-pw-highlight')).toBeVisible();
|
||||
const box1 = roundBox(await cancelButton.boundingBox());
|
||||
const box2 = roundBox(await recorder.page.locator('x-pw-highlight').boundingBox());
|
||||
expect(box1).toEqual(box2);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -145,7 +145,7 @@ it.describe('permissions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should support clipboard read', async ({ page, context, server, browserName, isWindows, isLinux, headless, channel }) => {
|
||||
it('should support clipboard read', async ({ page, context, server, browserName, isWindows, isLinux, headless, isHeadlessShell }) => {
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/27475' });
|
||||
it.fail(browserName === 'firefox', 'No such permissions (requires flag) in Firefox');
|
||||
it.fixme(browserName === 'webkit' && isWindows, 'WebPasteboardProxy::allPasteboardItemInfo not implemented for Windows.');
|
||||
|
|
@ -156,8 +156,7 @@ it('should support clipboard read', async ({ page, context, server, browserName,
|
|||
if (browserName !== 'webkit')
|
||||
expect(await getPermission(page, 'clipboard-read')).toBe('prompt');
|
||||
|
||||
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
if (browserName === 'chromium' && isHeadlessShell) {
|
||||
if (isHeadlessShell) {
|
||||
// Chromium (but not headless-shell) shows a dialog and does not resolve the promise.
|
||||
const error = await page.evaluate(() => navigator.clipboard.readText()).catch(e => e);
|
||||
expect(error.toString()).toContain('denied');
|
||||
|
|
|
|||
|
|
@ -22,8 +22,7 @@ import { verifyViewport } from '../config/utils';
|
|||
browserTest.describe('page screenshot', () => {
|
||||
browserTest.skip(({ browserName, headless }) => browserName === 'firefox' && !headless, 'Firefox headed produces a different image.');
|
||||
|
||||
browserTest('should run in parallel in multiple pages', async ({ server, contextFactory, browserName, headless, channel }) => {
|
||||
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
browserTest('should run in parallel in multiple pages', async ({ server, contextFactory, browserName, isHeadlessShell }) => {
|
||||
browserTest.fixme(browserName === 'chromium' && !isHeadlessShell, 'https://github.com/microsoft/playwright/issues/33330');
|
||||
|
||||
const context = await contextFactory();
|
||||
|
|
|
|||
|
|
@ -1510,7 +1510,7 @@ test('canvas clipping', async ({ runAndTrace, page, server }) => {
|
|||
});
|
||||
|
||||
const msg = await traceViewer.page.waitForEvent('console', { predicate: msg => msg.text().startsWith('canvas drawn:') });
|
||||
expect(msg.text()).toEqual('canvas drawn: [0,91,12,111]');
|
||||
expect(msg.text()).toEqual('canvas drawn: [0,91,11,20]');
|
||||
|
||||
const snapshot = await traceViewer.snapshotFrame('page.goto');
|
||||
await expect(snapshot.locator('canvas')).toHaveAttribute('title', `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`);
|
||||
|
|
|
|||
|
|
@ -429,8 +429,7 @@ for (const params of [
|
|||
height: 768,
|
||||
}
|
||||
]) {
|
||||
browserTest(`should produce screencast frames ${params.id}`, async ({ video, contextFactory, browserName, platform, headless, channel }, testInfo) => {
|
||||
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
browserTest(`should produce screencast frames ${params.id}`, async ({ video, contextFactory, browserName, platform, headless, isHeadlessShell }, testInfo) => {
|
||||
browserTest.skip(browserName === 'chromium' && video === 'on', 'Same screencast resolution conflicts');
|
||||
browserTest.fixme(browserName === 'chromium' && !isHeadlessShell, 'Chromium (but not headless-shell) screencast has a min width issue');
|
||||
browserTest.fixme(params.id === 'fit' && browserName === 'chromium' && platform === 'darwin', 'High DPI maxes image at 600x600');
|
||||
|
|
|
|||
|
|
@ -473,9 +473,8 @@ it.describe('screencast', () => {
|
|||
expect(videoFiles.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should scale frames down to the requested size ', async ({ browser, browserName, server, headless, channel }, testInfo) => {
|
||||
it('should scale frames down to the requested size ', async ({ browser, browserName, server, headless, isHeadlessShell }, testInfo) => {
|
||||
it.fixme(!headless, 'Fails on headed');
|
||||
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
it.fixme(browserName === 'chromium' && !isHeadlessShell, 'Chromium (but not headless shell) has a min width issue');
|
||||
|
||||
const context = await browser.newContext({
|
||||
|
|
@ -723,9 +722,8 @@ it.describe('screencast', () => {
|
|||
expect(files.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should capture full viewport', async ({ browserType, browserName, isWindows, headless, channel }, testInfo) => {
|
||||
it('should capture full viewport', async ({ browserType, browserName, isWindows, headless, isHeadlessShell }, testInfo) => {
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/22411' });
|
||||
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
it.fixme(browserName === 'chromium' && !isHeadlessShell, 'The square is not on the video');
|
||||
it.fixme(browserName === 'firefox' && isWindows, 'https://github.com/microsoft/playwright/issues/14405');
|
||||
const size = { width: 600, height: 400 };
|
||||
|
|
@ -759,9 +757,8 @@ it.describe('screencast', () => {
|
|||
expectAll(pixels, almostRed);
|
||||
});
|
||||
|
||||
it('should capture full viewport on hidpi', async ({ browserType, browserName, headless, isWindows, isLinux, channel }, testInfo) => {
|
||||
it('should capture full viewport on hidpi', async ({ browserType, browserName, headless, isWindows, isLinux, isHeadlessShell }, testInfo) => {
|
||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/22411' });
|
||||
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
it.fixme(browserName === 'chromium' && !isHeadlessShell, 'The square is not on the video');
|
||||
it.fixme(browserName === 'firefox' && isWindows, 'https://github.com/microsoft/playwright/issues/14405');
|
||||
it.fixme(browserName === 'webkit' && isLinux && !headless, 'https://github.com/microsoft/playwright/issues/22617');
|
||||
|
|
@ -797,10 +794,9 @@ it.describe('screencast', () => {
|
|||
expectAll(pixels, almostRed);
|
||||
});
|
||||
|
||||
it('should work with video+trace', async ({ browser, trace, headless, browserName, channel }, testInfo) => {
|
||||
it('should work with video+trace', async ({ browser, trace, headless, browserName, isHeadlessShell }, testInfo) => {
|
||||
it.skip(trace === 'on');
|
||||
it.fixme(!headless, 'different trace screencast image size on all browsers');
|
||||
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
|
||||
it.fixme(browserName === 'chromium' && !isHeadlessShell, 'different trace screencast image size');
|
||||
|
||||
const size = { width: 500, height: 400 };
|
||||
|
|
|
|||
|
|
@ -431,6 +431,9 @@ test('toHaveAccessibleName', async ({ page }) => {
|
|||
await expect(page.locator('div')).toHaveAccessibleName(/ell\w/);
|
||||
await expect(page.locator('div')).not.toHaveAccessibleName(/hello/);
|
||||
await expect(page.locator('div')).toHaveAccessibleName(/hello/, { ignoreCase: true });
|
||||
|
||||
await page.setContent(`<button>foo bar\nbaz</button>`);
|
||||
await expect(page.locator('button')).toHaveAccessibleName('foo bar baz');
|
||||
});
|
||||
|
||||
test('toHaveAccessibleDescription', async ({ page }) => {
|
||||
|
|
@ -443,6 +446,12 @@ test('toHaveAccessibleDescription', async ({ page }) => {
|
|||
await expect(page.locator('div')).toHaveAccessibleDescription(/ell\w/);
|
||||
await expect(page.locator('div')).not.toHaveAccessibleDescription(/hello/);
|
||||
await expect(page.locator('div')).toHaveAccessibleDescription(/hello/, { ignoreCase: true });
|
||||
|
||||
await page.setContent(`
|
||||
<div role="button" aria-describedby="desc"></div>
|
||||
<span id="desc">foo bar\nbaz</span>
|
||||
`);
|
||||
await expect(page.locator('div')).toHaveAccessibleDescription('foo bar baz');
|
||||
});
|
||||
|
||||
test('toHaveRole', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -24,12 +24,15 @@ function trimPatch(patch: string) {
|
|||
return patch.split('\n').map(line => line.trimEnd()).join('\n');
|
||||
}
|
||||
|
||||
test('should update snapshot with the update-snapshots flag', async ({ runInlineTest }, testInfo) => {
|
||||
test('should update snapshot with the update-snapshots flag with multiple projects', async ({ runInlineTest }, testInfo) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
export default { projects: [{ name: 'p1' }, { name: 'p2' }] };
|
||||
`,
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', async ({ page }) => {
|
||||
await page.setContent(\`<h1>hello</h1>\`);
|
||||
await page.setContent(\`<h1>hello</h1><h2>bye</h2>\`);
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||
- heading "world"
|
||||
\`);
|
||||
|
|
@ -43,12 +46,13 @@ test('should update snapshot with the update-snapshots flag', async ({ runInline
|
|||
expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
|
||||
--- a/a.spec.ts
|
||||
+++ b/a.spec.ts
|
||||
@@ -3,7 +3,7 @@
|
||||
@@ -3,7 +3,8 @@
|
||||
test('test', async ({ page }) => {
|
||||
await page.setContent(\`<h1>hello</h1>\`);
|
||||
await page.setContent(\`<h1>hello</h1><h2>bye</h2>\`);
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot(\`
|
||||
- - heading "world"
|
||||
+ - heading "hello" [level=1]
|
||||
+ - heading "bye" [level=2]
|
||||
\`);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue