Compare commits

...

30 commits

Author SHA1 Message Date
Max Schmitt c836cefb12
cherry-pick(1.49): remove old CDNs (#34100) 2024-12-27 13:43:09 +01:00
Max Schmitt 88bc8afc78
chore: mark v1.49.1 (#33921) 2024-12-09 17:25:25 -08:00
Dmitry Gozman 9e855d5b9a
cherry-pick(#33821): fix(recorder): allow clearing when recording is disabled (#33900)
Co-authored-by: Simon Knott <info@simonknott.de>
2024-12-06 13:44:10 -08:00
Dmitry Gozman 9365eb5dac
cherry-pick(#33834): fix: do not stall waiting for pending navigations after beforeunload dismiss (#33856) 2024-12-03 20:18:35 -08:00
Dmitry Gozman f92b2339fe cherry-pick(#33822): fix(codegen): do not reset current tool upon clearing highlight 2024-12-03 09:27:13 -08:00
Simon Knott 008722b2d9
cherry-pick(#33797): fix(trace): in indexTree check isVisible before adding to result (#33797) 2024-11-28 14:13:29 +01:00
Pavel Feldman 1dc8b3cbdc cherry-pick(#33746): chore: pin typescript while vue-tsc is broken 2024-11-28 13:12:20 +01:00
Dmitry Gozman fbc770c804
cherry-pick(#33793): fix(aria): escape even more yaml (#33795) 2024-11-28 04:09:28 -08:00
Dmitry Gozman 1046fe0455 cherry-pick(#33753): docs: update extensions doc for new headless 2024-11-28 13:07:20 +01:00
Dmitry Gozman 1781bf35b3 cherry-pick(#33706): docs: release notes for languages v1.49 2024-11-21 17:42:52 +00:00
Max Schmitt b52a21030f cherry-pick(#33712): docs(python): add LocatorAssertions.NotToMatchAriaSnapshot 2024-11-21 15:54:07 +01:00
Simon Knott 2128fac196 cherry-pick(#33693): docs: add video for 1.49 2024-11-21 11:09:53 +01:00
Simon Knott 2ba644852b cherry-pick(#33668): docs(aria): add demo video 2024-11-21 11:09:18 +01:00
Josh Kelley 4b0eca4d22 cherry-pick(#33680): docs: add docs for 1.49.0's new "chromium" option 2024-11-21 09:04:55 +00:00
Pavel Feldman e3c5986c5b cherry-pick(#33686): chore: escape more yaml values 2024-11-19 17:10:49 -08:00
Dmitry Gozman b3aaee0248
cherry-pick(#33667): fix(role): ignore invalid aria-labelledby attributes (#33672) 2024-11-19 05:13:05 -08:00
Max Schmitt 120cdf664b cherry-pick(#33662): fix: dark-mode in UI Mode 2024-11-19 10:29:29 +01:00
Max Schmitt a70a96ab25
chore: mark v1.49.0 (#33649) 2024-11-18 19:31:20 +01:00
Pavel Feldman 53f51a8cf1 cherry-pick(#33638): chore: clear highlight when performing action 2024-11-16 07:57:30 -08:00
Pavel Feldman 2a00ca8453 cherry-pick(#33635): chore: add cm placeholder text 2024-11-15 16:59:49 -08:00
Pavel Feldman 0e6434013b cherry-pick(#33632): chore: highlight edited locator while recording 2024-11-15 14:22:08 -08:00
Dmitry Gozman cb0f456e46
cherry-pick(#33629): fix(rebase): do not apply multiple rebaselines to the same assertion (#33630) 2024-11-15 12:49:54 -08:00
Max Schmitt 698823a78e cherry-pick(#33627): fix(codegen): document.documentElement is null on early navigation 2024-11-15 17:16:41 +01:00
Dmitry Gozman c0fa804367
cherry-pick(#33619): fix(aria): normalize whitespace in toMatchAccessible{Name,Description} (#33621) 2024-11-15 04:06:44 -08:00
Yury Semikhatsky 7a32228aed
cherry-pick(#33614): docs: add ariaSnapshot.timeout for language ports (#33615) 2024-11-14 12:40:44 -08:00
Simon Knott 0e31acea8f
cherry-pick(#33575): fix(canvas snapshots): position mismatch in headless mode 2024-11-14 15:43:48 +01:00
Dmitry Gozman b2a39ffc61 cherry-pick(#33604): docs: update docs about headless shell 2024-11-14 13:40:37 +00:00
Dmitry Gozman 1eea46bd66 cherry-pick(#33603): chore: update headless shell treatment 2024-11-14 13:39:03 +00:00
Dmitry Gozman 4c53e56cb4
cherry-pick(#33583): fix(merge): update error.cause location (#33601) 2024-11-14 03:03:34 -08:00
Pavel Feldman 3f36d7ff51 cherry-pick(#33594): chore: allow highlighting aria template from extension 2024-11-13 21:34:50 -08:00
84 changed files with 963 additions and 357 deletions

View file

@ -268,29 +268,8 @@ jobs:
- run: npx playwright install-deps - run: npx playwright install-deps
- run: utils/build/build-playwright-driver.sh - run: utils/build/build-playwright-driver.sh
test_linux_chromium_headless_shell: test_channel_chromium:
name: Chromium Headless Shell name: Test channel=chromium
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
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }} environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
strategy: strategy:
fail-fast: false fail-fast: false
@ -301,11 +280,13 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ./.github/actions/run-test - uses: ./.github/actions/run-test
with: 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 browsers-to-install: chromium
command: npm run ctest 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-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }} flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }} flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
env: env:
PWTEST_CHANNEL: chromium-next PWTEST_CHANNEL: chromium

View file

@ -206,6 +206,9 @@ Below is the HTML markup and the respective ARIA snapshot:
- link "About" - link "About"
``` ```
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-%%
* since: v1.49
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-js-%% ### option: Locator.ariaSnapshot.timeout = %%-input-timeout-js-%%
* since: v1.49 * since: v1.49

View file

@ -442,6 +442,23 @@ Expected options currently selected.
### option: LocatorAssertions.NotToHaveValues.timeout = %%-csharp-java-python-assertions-timeout-%% ### option: LocatorAssertions.NotToHaveValues.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.23 * since: v1.23
## async method: LocatorAssertions.NotToMatchAriaSnapshot
* since: v1.49
* langs: python
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot`].
### param: LocatorAssertions.NotToMatchAriaSnapshot.expected
* since: v1.49
- `expected` <string>
### option: LocatorAssertions.NotToMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
* since: v1.49
### option: LocatorAssertions.NotToMatchAriaSnapshot.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.49
## async method: LocatorAssertions.toBeAttached ## async method: LocatorAssertions.toBeAttached
* since: v1.33 * since: v1.33
@ -2122,7 +2139,7 @@ await expect(page.locator('body')).toMatchAriaSnapshot(`
``` ```
```python async ```python async
await page.goto('https://demo.playwright.dev/todomvc/') await page.goto("https://demo.playwright.dev/todomvc/")
await expect(page.locator('body')).to_match_aria_snapshot(''' await expect(page.locator('body')).to_match_aria_snapshot('''
- heading "todos" - heading "todos"
- textbox "What needs to be done?" - textbox "What needs to be done?"
@ -2130,7 +2147,7 @@ await expect(page.locator('body')).to_match_aria_snapshot('''
``` ```
```python sync ```python sync
page.goto('https://demo.playwright.dev/todomvc/') page.goto("https://demo.playwright.dev/todomvc/")
expect(page.locator('body')).to_match_aria_snapshot(''' expect(page.locator('body')).to_match_aria_snapshot('''
- heading "todos" - heading "todos"
- textbox "What needs to be done?" - textbox "What needs to be done?"
@ -2159,3 +2176,6 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%% ### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
* since: v1.49 * since: v1.49
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.49

View file

@ -302,10 +302,10 @@ await test.step('Log in', async () => {
```java ```java
// All actions between group and groupEnd // All actions between group and groupEnd
// will be shown in the trace viewer as a group. // will be shown in the trace viewer as a group.
page.context().tracing.group("Open Playwright.dev > API"); page.context().tracing().group("Open Playwright.dev > API");
page.navigate("https://playwright.dev/"); page.navigate("https://playwright.dev/");
page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("API")).click(); page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("API")).click();
page.context().tracing.groupEnd(); page.context().tracing().groupEnd();
``` ```
```python sync ```python sync
@ -329,10 +329,10 @@ await page.context.tracing.group_end()
```csharp ```csharp
// All actions between GroupAsync and GroupEndAsync // All actions between GroupAsync and GroupEndAsync
// will be shown in the trace viewer as a group. // will be shown in the trace viewer as a group.
await Page.Context().Tracing.GroupAsync("Open Playwright.dev > API"); await Page.Context.Tracing.GroupAsync("Open Playwright.dev > API");
await Page.GotoAsync("https://playwright.dev/"); await Page.GotoAsync("https://playwright.dev/");
await Page.GetByRole(AriaRole.Link, new() { Name = "API" }).ClickAsync(); await Page.GetByRole(AriaRole.Link, new() { Name = "API" }).ClickAsync();
await Page.Context().Tracing.GroupEndAsync(); await Page.Context.Tracing.GroupEndAsync();
``` ```
### param: Tracing.group.name ### param: Tracing.group.name

View file

@ -1001,7 +1001,11 @@ Additional arguments to pass to the browser instance. The list of Chromium flags
## browser-option-channel ## browser-option-channel
- `channel` <[string]> - `channel` <[string]>
Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", "msedge-canary". Read more about using [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge). Browser distribution channel.
Use "chromium" to [opt in to new headless mode](../browsers.md#opt-in-to-new-headless-mode).
Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or "msedge-canary" to use branded [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge).
## browser-option-chromiumsandbox ## browser-option-chromiumsandbox
- `chromiumSandbox` <[boolean]> - `chromiumSandbox` <[boolean]>

View file

@ -2,6 +2,7 @@
id: aria-snapshots id: aria-snapshots
title: "Aria snapshots" title: "Aria snapshots"
--- ---
import LiteYouTube from '@site/src/components/LiteYouTube';
## Overview ## Overview
@ -9,6 +10,11 @@ In Playwright, aria snapshots provide a YAML representation of the accessibility
These snapshots can be stored and compared later to verify if the page structure remains consistent or meets defined These snapshots can be stored and compared later to verify if the page structure remains consistent or meets defined
expectations. expectations.
<LiteYouTube
id="P4R6hnsE0UY"
title="Getting started with ARIA Snapshots"
/>
The YAML format describes the hierarchical structure of accessible elements on the page, detailing **roles**, **attributes**, **values**, and **text content**. The YAML format describes the hierarchical structure of accessible elements on the page, detailing **roles**, **attributes**, **values**, and **text content**.
The structure follows a tree-like syntax, where each node represents an accessible element, and indentation indicates The structure follows a tree-like syntax, where each node represents an accessible element, and indentation indicates
nested elements. nested elements.
@ -61,19 +67,19 @@ await expect(page.locator('body')).toMatchAriaSnapshot(`
``` ```
```python sync ```python sync
page.locator("body").to_match_aria_snapshot(""" expect(page.locator("body")).to_match_aria_snapshot("""
- heading "title" - heading "title"
""") """)
``` ```
```python async ```python async
await page.locator("body").to_match_aria_snapshot(""" await expect(page.locator("body")).to_match_aria_snapshot("""
- heading "title" - heading "title"
""") """)
``` ```
```java ```java
page.locator("body").expect().toMatchAriaSnapshot(""" assertThat(page.locator("body")).matchesAriaSnapshot("""
- heading "title" - heading "title"
"""); """);
``` ```

View file

@ -338,37 +338,92 @@ 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. 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 ```bash js
# When only running tests headlessly # only running tests headlessly
npx playwright install chromium-headless-shell firefox webkit npx playwright install --with-deps --only-shell
``` ```
```bash java ```bash java
# When only running tests headlessly # only running tests headlessly
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install chromium-headless-shell firefox webkit" mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps --only-shell"
``` ```
```bash python ```bash python
# When only running tests headlessly # only running tests headlessly
playwright install chromium-headless-shell firefox webkit playwright install --with-deps --only-shell
``` ```
```bash csharp ```bash csharp
# When only running tests headlessly # only running tests headlessly
pwsh bin/Debug/netX/playwright.ps1 install chromium-headless-shell firefox webkit 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 ### Google Chrome & Microsoft Edge
While Playwright can download and use the recent Chromium build, it can operate against the branded Google Chrome and Microsoft Edge browsers available on the machine (note that Playwright doesn't install them by default). In particular, the current Playwright version will support Stable and Beta channels of these browsers. While Playwright can download and use the recent Chromium build, it can operate against the branded Google Chrome and Microsoft Edge browsers available on the machine (note that Playwright doesn't install them by default). In particular, the current Playwright version will support Stable and Beta channels of these browsers.
Available channels are `chrome`, `msedge`, `chrome-beta`, `msedge-beta` or `msedge-dev`. Available channels are `chrome`, `msedge`, `chrome-beta`, `msedge-beta`, `chrome-dev`, `msedge-dev`, `chrome-canary`, `msedge-canary`.
:::warning :::warning
Certain Enterprise Browser Policies may impact Playwright's ability to launch and control Google Chrome and Microsoft Edge. Running in an environment with browser policies is outside of the Playwright project's scope. Certain Enterprise Browser Policies may impact Playwright's ability to launch and control Google Chrome and Microsoft Edge. Running in an environment with browser policies is outside of the Playwright project's scope.

View file

@ -214,16 +214,15 @@ def test_popup_page(page: Page, extension_id: str) -> None:
## Headless mode ## Headless mode
By default, Chrome's headless mode in Playwright does not support Chrome extensions. To overcome this limitation, you can run Chrome's persistent context with a new headless mode by using the following code: By default, Chrome's headless mode in Playwright does not support Chrome extensions. To overcome this limitation, you can run Chrome's persistent context with a new headless mode by using [channel `chromium`](./browsers.md#opt-in-to-new-headless-mode):
```js title="fixtures.ts" ```js title="fixtures.ts"
// ... // ...
const pathToExtension = path.join(__dirname, 'my-extension'); const pathToExtension = path.join(__dirname, 'my-extension');
const context = await chromium.launchPersistentContext('', { const context = await chromium.launchPersistentContext('', {
headless: false, channel: 'chromium',
args: [ args: [
`--headless=new`,
`--disable-extensions-except=${pathToExtension}`, `--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`, `--load-extension=${pathToExtension}`,
], ],
@ -235,9 +234,8 @@ const context = await chromium.launchPersistentContext('', {
path_to_extension = Path(__file__).parent.joinpath("my-extension") path_to_extension = Path(__file__).parent.joinpath("my-extension")
context = playwright.chromium.launch_persistent_context( context = playwright.chromium.launch_persistent_context(
"", "",
headless=False, channel="chromium",
args=[ args=[
"--headless=new",
f"--disable-extensions-except={path_to_extension}", f"--disable-extensions-except={path_to_extension}",
f"--load-extension={path_to_extension}", f"--load-extension={path_to_extension}",
], ],

View file

@ -5,6 +5,91 @@ toc_max_heading_level: 2
--- ---
## Version 1.49
### Aria snapshots
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
```csharp
await page.GotoAsync("https://playwright.dev");
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(@"
- banner:
- heading /Playwright enables reliable/ [level=1]
- link ""Get started""
- link ""Star microsoft/playwright on GitHub""
- main:
- img ""Browsers (Chromium, Firefox, WebKit)""
- heading ""Any browser • Any platform • One API""
");
```
You can generate this assertion with [Test Generator](./codegen) or by calling [`method: Locator.ariaSnapshot`].
Learn more in the [aria snapshots guide](./aria-snapshots).
### Tracing groups
New method [`method: Tracing.group`] allows you to visually group actions in the trace viewer.
```csharp
// All actions between GroupAsync and GroupEndAsync
// will be shown in the trace viewer as a group.
await Page.Context.Tracing.GroupAsync("Open Playwright.dev > API");
await Page.GotoAsync("https://playwright.dev/");
await Page.GetByRole(AriaRole.Link, new() { Name = "API" }).ClickAsync();
await Page.Context.Tracing.GroupEndAsync();
```
### Breaking: `chrome` and `msedge` channels switch to new headless mode
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`
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.
### 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.
```xml csharp title="runsettings.xml"
<?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
```
### Miscellaneous
- There will be no more updates for WebKit on Ubuntu 20.04 and Debian 11. We recommend updating your OS to a later version.
- `<canvas>` elements inside a snapshot now draw a preview.
### Browser Versions
- Chromium 131.0.6778.33
- Mozilla Firefox 132.0
- WebKit 18.2
This version was also tested against the following stable channels:
- Google Chrome 130
- Microsoft Edge 130
## Version 1.48 ## Version 1.48
### WebSocket routing ### WebSocket routing

View file

@ -4,6 +4,79 @@ title: "Release notes"
toc_max_heading_level: 2 toc_max_heading_level: 2
--- ---
## Version 1.49
### Aria snapshots
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
```java
page.navigate("https://playwright.dev");
assertThat(page.locator("body")).matchesAriaSnapshot("""
- banner:
- heading /Playwright enables reliable/ [level=1]
- link "Get started"
- link "Star microsoft/playwright on GitHub"
- main:
- img "Browsers (Chromium, Firefox, WebKit)"
- heading "Any browser • Any platform • One API"
""");
```
You can generate this assertion with [Test Generator](./codegen) or by calling [`method: Locator.ariaSnapshot`].
Learn more in the [aria snapshots guide](./aria-snapshots).
### Tracing groups
New method [`method: Tracing.group`] allows you to visually group actions in the trace viewer.
```java
// All actions between group and groupEnd
// will be shown in the trace viewer as a group.
page.context().tracing().group("Open Playwright.dev > API");
page.navigate("https://playwright.dev/");
page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("API")).click();
page.context().tracing().groupEnd();
```
### Breaking: `chrome` and `msedge` channels switch to new headless mode
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`
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.
### 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.
```java
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setChannel("chromium"));
```
### Miscellaneous
- There will be no more updates for WebKit on Ubuntu 20.04 and Debian 11. We recommend updating your OS to a later version.
- `<canvas>` elements inside a snapshot now draw a preview.
### Browser Versions
- Chromium 131.0.6778.33
- Mozilla Firefox 132.0
- WebKit 18.2
This version was also tested against the following stable channels:
- Google Chrome 130
- Microsoft Edge 130
## Version 1.48 ## Version 1.48
### WebSocket routing ### WebSocket routing

View file

@ -8,6 +8,11 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
## Version 1.49 ## Version 1.49
<LiteYouTube
id="S5wCft-ImKk"
title="Playwright 1.49"
/>
### Aria snapshots ### Aria snapshots
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML. New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
@ -40,51 +45,13 @@ Learn more in the [aria snapshots guide](./aria-snapshots).
### Breaking: channels `chrome`, `msedge` and similar switch to new headless ### 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`
![Chromium Headless](https://github.com/user-attachments/assets/2829e86a-dfe2-4743-a6d4-2aa65beea890) #### 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. 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.
#### 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',
},
},
],
});
```
### Other breaking changes ### Other breaking changes
@ -92,6 +59,27 @@ export default defineConfig({
- Package `@playwright/experimental-ct-vue2` will no longer be updated. - Package `@playwright/experimental-ct-vue2` will no longer be updated.
- Package `@playwright/experimental-ct-solid` 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 ### Miscellaneous
- `<canvas>` elements inside a snapshot now draw a preview. - `<canvas>` elements inside a snapshot now draw a preview.

View file

@ -4,6 +4,80 @@ title: "Release notes"
toc_max_heading_level: 2 toc_max_heading_level: 2
--- ---
## Version 1.49
### Aria snapshots
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
```python
page.goto("https://playwright.dev")
expect(page.locator('body')).to_match_aria_snapshot('''
- banner:
- heading /Playwright enables reliable/ [level=1]
- link "Get started"
- link "Star microsoft/playwright on GitHub"
- main:
- img "Browsers (Chromium, Firefox, WebKit)"
- heading "Any browser • Any platform • One API"
''')
```
You can generate this assertion with [Test Generator](./codegen) or by calling [`method: Locator.ariaSnapshot`].
Learn more in the [aria snapshots guide](./aria-snapshots).
### Tracing groups
New method [`method: Tracing.group`] allows you to visually group actions in the trace viewer.
```python
# All actions between group and group_end
# will be shown in the trace viewer as a group.
page.context.tracing.group("Open Playwright.dev > API")
page.goto("https://playwright.dev/")
page.get_by_role("link", name="API").click()
page.context.tracing.group_end()
```
### Breaking: `chrome` and `msedge` channels switch to new headless mode
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`
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.
### 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.
```bash python
pytest test_login.py --browser-channel chromium
```
### Miscellaneous
- There will be no more updates for WebKit on Ubuntu 20.04 and Debian 11. We recommend updating your OS to a later version.
- `<canvas>` elements inside a snapshot now draw a preview.
- Python 3.8 is not supported anymore.
### Browser Versions
- Chromium 131.0.6778.33
- Mozilla Firefox 132.0
- WebKit 18.2
This version was also tested against the following stable channels:
- Google Chrome 130
- Microsoft Edge 130
## Version 1.48 ## Version 1.48
### WebSocket routing ### WebSocket routing

60
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "playwright-internal", "name": "playwright-internal",
"version": "1.49.0-next", "version": "1.49.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "playwright-internal", "name": "playwright-internal",
"version": "1.49.0-next", "version": "1.49.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@ -7733,10 +7733,10 @@
"version": "0.0.0" "version": "0.0.0"
}, },
"packages/playwright": { "packages/playwright": {
"version": "1.49.0-next", "version": "1.49.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.49.0-next" "playwright-core": "1.49.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7750,11 +7750,11 @@
}, },
"packages/playwright-browser-chromium": { "packages/playwright-browser-chromium": {
"name": "@playwright/browser-chromium", "name": "@playwright/browser-chromium",
"version": "1.49.0-next", "version": "1.49.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.49.0-next" "playwright-core": "1.49.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -7762,11 +7762,11 @@
}, },
"packages/playwright-browser-firefox": { "packages/playwright-browser-firefox": {
"name": "@playwright/browser-firefox", "name": "@playwright/browser-firefox",
"version": "1.49.0-next", "version": "1.49.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.49.0-next" "playwright-core": "1.49.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -7774,22 +7774,22 @@
}, },
"packages/playwright-browser-webkit": { "packages/playwright-browser-webkit": {
"name": "@playwright/browser-webkit", "name": "@playwright/browser-webkit",
"version": "1.49.0-next", "version": "1.49.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.49.0-next" "playwright-core": "1.49.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"packages/playwright-chromium": { "packages/playwright-chromium": {
"version": "1.49.0-next", "version": "1.49.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.49.0-next" "playwright-core": "1.49.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7799,7 +7799,7 @@
} }
}, },
"packages/playwright-core": { "packages/playwright-core": {
"version": "1.49.0-next", "version": "1.49.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
@ -7810,11 +7810,11 @@
}, },
"packages/playwright-ct-core": { "packages/playwright-ct-core": {
"name": "@playwright/experimental-ct-core", "name": "@playwright/experimental-ct-core",
"version": "1.49.0-next", "version": "1.49.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.49.0-next", "playwright": "1.49.1",
"playwright-core": "1.49.0-next", "playwright-core": "1.49.1",
"vite": "^5.2.8" "vite": "^5.2.8"
}, },
"engines": { "engines": {
@ -7823,10 +7823,10 @@
}, },
"packages/playwright-ct-react": { "packages/playwright-ct-react": {
"name": "@playwright/experimental-ct-react", "name": "@playwright/experimental-ct-react",
"version": "1.49.0-next", "version": "1.49.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next", "@playwright/experimental-ct-core": "1.49.1",
"@vitejs/plugin-react": "^4.2.1" "@vitejs/plugin-react": "^4.2.1"
}, },
"bin": { "bin": {
@ -7838,10 +7838,10 @@
}, },
"packages/playwright-ct-react17": { "packages/playwright-ct-react17": {
"name": "@playwright/experimental-ct-react17", "name": "@playwright/experimental-ct-react17",
"version": "1.49.0-next", "version": "1.49.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next", "@playwright/experimental-ct-core": "1.49.1",
"@vitejs/plugin-react": "^4.2.1" "@vitejs/plugin-react": "^4.2.1"
}, },
"bin": { "bin": {
@ -7853,10 +7853,10 @@
}, },
"packages/playwright-ct-svelte": { "packages/playwright-ct-svelte": {
"name": "@playwright/experimental-ct-svelte", "name": "@playwright/experimental-ct-svelte",
"version": "1.49.0-next", "version": "1.49.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next", "@playwright/experimental-ct-core": "1.49.1",
"@sveltejs/vite-plugin-svelte": "^3.0.1" "@sveltejs/vite-plugin-svelte": "^3.0.1"
}, },
"bin": { "bin": {
@ -7871,10 +7871,10 @@
}, },
"packages/playwright-ct-vue": { "packages/playwright-ct-vue": {
"name": "@playwright/experimental-ct-vue", "name": "@playwright/experimental-ct-vue",
"version": "1.49.0-next", "version": "1.49.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next", "@playwright/experimental-ct-core": "1.49.1",
"@vitejs/plugin-vue": "^4.2.1" "@vitejs/plugin-vue": "^4.2.1"
}, },
"bin": { "bin": {
@ -7885,11 +7885,11 @@
} }
}, },
"packages/playwright-firefox": { "packages/playwright-firefox": {
"version": "1.49.0-next", "version": "1.49.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.49.0-next" "playwright-core": "1.49.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7900,10 +7900,10 @@
}, },
"packages/playwright-test": { "packages/playwright-test": {
"name": "@playwright/test", "name": "@playwright/test",
"version": "1.49.0-next", "version": "1.49.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.49.0-next" "playwright": "1.49.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -7913,11 +7913,11 @@
} }
}, },
"packages/playwright-webkit": { "packages/playwright-webkit": {
"version": "1.49.0-next", "version": "1.49.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.49.0-next" "playwright-core": "1.49.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"

View file

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

View file

@ -24,4 +24,4 @@ try {
} }
if (install) if (install)
install(['chromium', 'ffmpeg']); install(['chromium', 'chromium-headless-shell', 'ffmpeg']);

View file

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

View file

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

View file

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

View file

@ -24,4 +24,4 @@ try {
} }
if (install) if (install)
install(['chromium', 'ffmpeg']); install(['chromium', 'chromium-headless-shell', 'ffmpeg']);

View file

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

View file

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

View file

@ -96,16 +96,42 @@ function suggestedBrowsersToInstall() {
return registry.executables().filter(e => e.installType !== 'none' && e.type !== 'tool').map(e => e.name).join(', '); 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 faultyArguments: string[] = [];
const executables: Executable[] = []; const executables: Executable[] = [];
for (const arg of args) { const handleArgument = (arg: string) => {
const executable = registry.findExecutable(arg); const executable = registry.findExecutable(arg);
if (!executable || executable.installType === 'none') if (!executable || executable.installType === 'none')
faultyArguments.push(arg); faultyArguments.push(arg);
else else
executables.push(executable); 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) if (faultyArguments.length)
throw new Error(`Invalid installation targets: ${faultyArguments.map(name => `'${name}'`).join(', ')}. Expecting one of: ${suggestedBrowsersToInstall()}`); throw new Error(`Invalid installation targets: ${faultyArguments.map(name => `'${name}'`).join(', ')}. Expecting one of: ${suggestedBrowsersToInstall()}`);
return executables; return executables;
@ -118,7 +144,12 @@ program
.option('--with-deps', 'install system dependencies for browsers') .option('--with-deps', 'install system dependencies for browsers')
.option('--dry-run', 'do not execute installation, only print information') .option('--dry-run', 'do not execute installation, only print information')
.option('--force', 'force reinstall of stable browser channels') .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()) { if (isLikelyNpxGlobal()) {
console.error(wrapInASCIIBox([ console.error(wrapInASCIIBox([
`WARNING: It looks like you are running 'npx playwright install' without first`, `WARNING: It looks like you are running 'npx playwright install' without first`,
@ -141,7 +172,7 @@ program
} }
try { try {
const hasNoArguments = !args.length; const hasNoArguments = !args.length;
const executables = hasNoArguments ? registry.defaultExecutables() : checkBrowsersToInstall(args); const executables = hasNoArguments ? defaultBrowsersToInstall(options) : checkBrowsersToInstall(args, options);
if (options.withDeps) if (options.withDeps)
await registry.installDeps(executables, !!options.dryRun); await registry.installDeps(executables, !!options.dryRun);
if (options.dryRun) { if (options.dryRun) {
@ -199,9 +230,9 @@ program
.action(async function(args: string[], options: { dryRun?: boolean }) { .action(async function(args: string[], options: { dryRun?: boolean }) {
try { try {
if (!args.length) if (!args.length)
await registry.installDeps(registry.defaultExecutables(), !!options.dryRun); await registry.installDeps(defaultBrowsersToInstall({}), !!options.dryRun);
else else
await registry.installDeps(checkBrowsersToInstall(args), !!options.dryRun); await registry.installDeps(checkBrowsersToInstall(args, {}), !!options.dryRun);
} catch (e) { } catch (e) {
console.log(`Failed to install browser dependencies\n${e}`); console.log(`Failed to install browser dependencies\n${e}`);
gracefullyProcessExitDoNotHang(1); gracefullyProcessExitDoNotHang(1);

View file

@ -422,7 +422,8 @@ scheme.DebugControllerSetRecorderModeParams = tObject({
}); });
scheme.DebugControllerSetRecorderModeResult = tOptional(tObject({})); scheme.DebugControllerSetRecorderModeResult = tOptional(tObject({}));
scheme.DebugControllerHighlightParams = tObject({ scheme.DebugControllerHighlightParams = tObject({
selector: tString, selector: tOptional(tString),
ariaTemplate: tOptional(tString),
}); });
scheme.DebugControllerHighlightResult = tOptional(tObject({})); scheme.DebugControllerHighlightResult = tOptional(tObject({}));
scheme.DebugControllerHideHighlightParams = tOptional(tObject({})); scheme.DebugControllerHideHighlightParams = tOptional(tObject({}));

View file

@ -15,12 +15,16 @@
*/ */
import { parseYamlTemplate } from '../utils/isomorphic/ariaSnapshot'; import { parseYamlTemplate } from '../utils/isomorphic/ariaSnapshot';
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot'; import type { AriaTemplateNode, ParsedYaml } from '@isomorphic/ariaSnapshot';
import { yaml } from '../utilsBundle'; import { yaml } from '../utilsBundle';
export function parseAriaSnapshot(text: string): AriaTemplateNode { export function parseAriaSnapshot(text: string): AriaTemplateNode {
const fragment = yaml.parse(text); return parseYamlTemplate(parseYamlForAriaSnapshot(text));
if (!Array.isArray(fragment)) }
throw new Error('Expected object key starting with "- ":\n\n' + text + '\n');
return parseYamlTemplate(fragment); 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;
} }

View file

@ -842,6 +842,9 @@ class FrameSession {
event.type, event.type,
event.message, event.message,
async (accept: boolean, promptText?: string) => { async (accept: boolean, promptText?: string) => {
// TODO: this should actually be a CDP event that notifies about a cancelled navigation attempt.
if (this._isMainFrame() && event.type === 'beforeunload' && !accept)
this._page._frameManager.frameAbortedNavigation(this._page.mainFrame()._id, 'navigation cancelled by beforeunload dialog');
await this._client.send('Page.handleJavaScriptDialog', { accept, promptText }); await this._client.send('Page.handleJavaScriptDialog', { accept, promptText });
}, },
event.defaultPrompt)); event.defaultPrompt));

View file

@ -24,6 +24,7 @@ import type { Playwright } from './playwright';
import { Recorder } from './recorder'; import { Recorder } from './recorder';
import { EmptyRecorderApp } from './recorder/recorderApp'; import { EmptyRecorderApp } from './recorder/recorderApp';
import { asLocator, type Language } from '../utils'; import { asLocator, type Language } from '../utils';
import { parseYamlForAriaSnapshot } from './ariaSnapshot';
const internalMetadata = serverSideCallMetadata(); const internalMetadata = serverSideCallMetadata();
@ -142,9 +143,13 @@ export class DebugController extends SdkObject {
this._autoCloseTimer = setTimeout(heartBeat, 30000); this._autoCloseTimer = setTimeout(heartBeat, 30000);
} }
async highlight(selector: string) { async highlight(params: { selector?: string, ariaTemplate?: string }) {
for (const recorder of await this._allRecorders()) for (const recorder of await this._allRecorders()) {
recorder.setHighlightedSelector(this._sdkLanguage, selector); if (params.ariaTemplate)
recorder.setHighlightedAriaTemplate(parseYamlForAriaSnapshot(params.ariaTemplate));
else if (params.selector)
recorder.setHighlightedSelector(this._sdkLanguage, params.selector);
}
} }
async hideHighlight() { async hideHighlight() {

View file

@ -68,7 +68,7 @@ export class DebugControllerDispatcher extends Dispatcher<DebugController, chann
} }
async highlight(params: channels.DebugControllerHighlightParams) { async highlight(params: channels.DebugControllerHighlightParams) {
await this._object.highlight(params.selector); await this._object.highlight(params);
} }
async hideHighlight() { async hideHighlight() {

View file

@ -19,7 +19,6 @@ export {
registry, registry,
registryDirectory, registryDirectory,
Registry, Registry,
installDefaultBrowsersForNpmInstall,
installBrowsersForNpmInstall, installBrowsersForNpmInstall,
writeDockerVersion } from './registry'; writeDockerVersion } from './registry';

View file

@ -90,7 +90,8 @@ export class Highlight {
} }
install() { 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); this._injectedScript.document.documentElement.appendChild(this._glassPaneElement);
} }

View file

@ -492,10 +492,11 @@ class RecordActionTool implements RecorderTool {
return; return;
const result = activeElement ? this._recorder.injectedScript.generateSelector(activeElement, { testIdAttributeName: this._recorder.state.testIdAttributeName }) : null; const result = activeElement ? this._recorder.injectedScript.generateSelector(activeElement, { testIdAttributeName: this._recorder.state.testIdAttributeName }) : null;
this._activeModel = result && result.selector ? result : null; this._activeModel = result && result.selector ? result : null;
if (userGesture) if (userGesture) {
this._hoveredElement = activeElement as HTMLElement | null; this._hoveredElement = activeElement as HTMLElement | null;
this._updateModelForHoveredElement(); this._updateModelForHoveredElement();
} }
}
private _shouldIgnoreMouseEvent(event: MouseEvent): boolean { private _shouldIgnoreMouseEvent(event: MouseEvent): boolean {
const target = this._recorder.deepEventTarget(event); const target = this._recorder.deepEventTarget(event);
@ -589,6 +590,8 @@ class RecordActionTool implements RecorderTool {
} }
private _updateModelForHoveredElement() { private _updateModelForHoveredElement() {
if (this._performingActions.size)
return;
if (!this._hoveredElement || !this._hoveredElement.isConnected) { if (!this._hoveredElement || !this._hoveredElement.isConnected) {
this._hoveredModel = null; this._hoveredModel = null;
this._hoveredElement = null; this._hoveredElement = null;
@ -1018,7 +1021,7 @@ export class Recorder {
private _listeners: (() => void)[] = []; private _listeners: (() => void)[] = [];
private _currentTool: RecorderTool; private _currentTool: RecorderTool;
private _tools: Record<Mode, RecorderTool>; private _tools: Record<Mode, RecorderTool>;
private _actionSelectorModel: HighlightModel | null = null; private _lastHighlightedSelector: string | undefined = undefined;
private _lastHighlightedAriaTemplateJSON: string = 'undefined'; private _lastHighlightedAriaTemplateJSON: string = 'undefined';
readonly highlight: Highlight; readonly highlight: Highlight;
readonly overlay: Overlay | undefined; readonly overlay: Overlay | undefined;
@ -1129,12 +1132,12 @@ export class Recorder {
this._switchCurrentTool(); this._switchCurrentTool();
this.overlay?.setUIState(state); this.overlay?.setUIState(state);
// Race or scroll. let highlight: HighlightModel | 'clear' | 'noop' = 'noop';
if (this._actionSelectorModel?.selector && !this._actionSelectorModel?.elements.length && !this._lastHighlightedAriaTemplateJSON) if (state.actionSelector !== this._lastHighlightedSelector) {
this._actionSelectorModel = null; this._lastHighlightedSelector = state.actionSelector;
const model = state.actionSelector ? querySelector(this.injectedScript, state.actionSelector, this.document) : null;
if (state.actionSelector && state.actionSelector !== this._actionSelectorModel?.selector) highlight = model?.elements.length ? model : 'clear';
this._actionSelectorModel = querySelector(this.injectedScript, state.actionSelector, this.document); }
const ariaTemplateJSON = JSON.stringify(state.ariaTemplate); const ariaTemplateJSON = JSON.stringify(state.ariaTemplate);
if (this._lastHighlightedAriaTemplateJSON !== ariaTemplateJSON) { if (this._lastHighlightedAriaTemplateJSON !== ariaTemplateJSON) {
@ -1142,20 +1145,18 @@ export class Recorder {
const template = state.ariaTemplate ? this.injectedScript.utils.parseYamlTemplate(state.ariaTemplate) : undefined; const template = state.ariaTemplate ? this.injectedScript.utils.parseYamlTemplate(state.ariaTemplate) : undefined;
const elements = template ? this.injectedScript.getAllByAria(this.document, template) : []; const elements = template ? this.injectedScript.getAllByAria(this.document, template) : [];
if (elements.length) if (elements.length)
this._actionSelectorModel = { elements }; highlight = { elements };
else else
this._actionSelectorModel = null; highlight = 'clear';
} }
if (!state.actionSelector && !state.ariaTemplate) if (highlight === 'clear')
this._actionSelectorModel = null; this.clearHighlight();
else if (highlight !== 'noop')
if (this.state.mode === 'none' || this.state.mode === 'standby') this.updateHighlight(highlight, false);
this.updateHighlight(this._actionSelectorModel, false);
} }
clearHighlight() { clearHighlight() {
this._currentTool.cleanup?.();
this.updateHighlight(null, false); this.updateHighlight(null, false);
} }
@ -1266,6 +1267,8 @@ export class Recorder {
private _onScroll(event: Event) { private _onScroll(event: Event) {
if (!event.isTrusted) if (!event.isTrusted)
return; return;
this._lastHighlightedSelector = undefined;
this._lastHighlightedAriaTemplateJSON = 'undefined';
this.highlight.hideActionPoint(); this.highlight.hideActionPoint();
this._currentTool.onScroll?.(event); this._currentTool.onScroll?.(event);
} }

View file

@ -383,7 +383,11 @@ export function getAriaLabelledByElements(element: Element): Element[] | null {
const ref = element.getAttribute('aria-labelledby'); const ref = element.getAttribute('aria-labelledby');
if (ref === null) if (ref === null)
return null; return null;
return getIdRefs(element, ref); const refs = getIdRefs(element, ref);
// step 2b:
// "if the current node has an aria-labelledby attribute that contains at least one valid IDREF"
// Therefore, if none of the refs match an element, we consider aria-labelledby to be missing.
return refs.length ? refs : null;
} }
function allowsNameFromContent(role: string, targetDescendant: boolean) { function allowsNameFromContent(role: string, targetDescendant: boolean) {

View file

@ -62,12 +62,8 @@ function yamlStringNeedsQuotes(str: string): boolean {
if (/^-\s/.test(str)) if (/^-\s/.test(str))
return true; return true;
// Strings that start with a special indicator character need quotes // Strings containing ':' or '\n' followed by a space or at the end need quotes
if (/^[&*].*/.test(str)) if (/[\n:](\s|$)/.test(str))
return true;
// Strings containing ':' followed by a space or at the end need quotes
if (/:(\s|$)/.test(str))
return true; return true;
// Strings containing '#' preceded by a space need quotes (comment indicator) // Strings containing '#' preceded by a space need quotes (comment indicator)
@ -78,21 +74,17 @@ function yamlStringNeedsQuotes(str: string): boolean {
if (/[\n\r]/.test(str)) if (/[\n\r]/.test(str))
return true; return true;
// Strings starting with '?' or '!' (directives) need quotes // Strings starting with indicator characters or quotes need quotes
if (/^[?!]/.test(str)) if (/^[&*\],?!>|@"'#%]/.test(str))
return true;
// Strings starting with '>' or '|' (block scalar indicators) need quotes
if (/^[>|]/.test(str))
return true;
// Strings starting with quotes need quotes
if (/^["']/.test(str))
return true; return true;
// Strings containing special characters that could cause ambiguity // Strings containing special characters that could cause ambiguity
if (/[{}`]/.test(str)) if (/[{}`]/.test(str))
return true; return true;
// Non-string types recognized by YAML
if (!isNaN(Number(str)) || ['y', 'n', 'yes', 'no', 'true', 'false', 'on', 'off', 'null'].includes(str.toLowerCase()))
return true;
return false; return false;
} }

View file

@ -43,12 +43,12 @@ export async function launchApp(browserType: BrowserType, options: {
} }
const context = await browserType.launchPersistentContext(serverSideCallMetadata(), '', { const context = await browserType.launchPersistentContext(serverSideCallMetadata(), '', {
channel: !options.persistentContextOptions?.executablePath ? findChromiumChannel(options.sdkLanguage) : undefined,
noDefaultViewport: true,
ignoreDefaultArgs: ['--enable-automation'], ignoreDefaultArgs: ['--enable-automation'],
colorScheme: 'no-override',
acceptDownloads: isUnderTest() ? 'accept' : 'internal-browser-default',
...options?.persistentContextOptions, ...options?.persistentContextOptions,
channel: options.persistentContextOptions?.channel ?? (!options.persistentContextOptions?.executablePath ? findChromiumChannel(options.sdkLanguage) : undefined),
noDefaultViewport: options.persistentContextOptions?.noDefaultViewport ?? true,
acceptDownloads: options?.persistentContextOptions?.acceptDownloads ?? (isUnderTest() ? 'accept' : 'internal-browser-default'),
colorScheme: options?.persistentContextOptions?.colorScheme ?? 'no-override',
args, args,
}); });
const [page] = context.pages(); const [page] = context.pages();

View file

@ -40,7 +40,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
readonly handleSIGINT: boolean | undefined; readonly handleSIGINT: boolean | undefined;
private _context: BrowserContext; private _context: BrowserContext;
private _mode: Mode; private _mode: Mode;
private _highlightedElement: { selector?: string, ariaSnapshot?: ParsedYaml } = {}; private _highlightedElement: { selector?: string, ariaTemplate?: ParsedYaml } = {};
private _overlayState: OverlayState = { offsetX: 0 }; private _overlayState: OverlayState = { offsetX: 0 };
private _recorderApp: IRecorderApp | null = null; private _recorderApp: IRecorderApp | null = null;
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>(); private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
@ -107,8 +107,8 @@ export class Recorder implements InstrumentationListener, IRecorder {
if (data.event === 'highlightRequested') { if (data.event === 'highlightRequested') {
if (data.params.selector) if (data.params.selector)
this.setHighlightedSelector(this._currentLanguage, data.params.selector); this.setHighlightedSelector(this._currentLanguage, data.params.selector);
if (data.params.ariaSnapshot) if (data.params.ariaTemplate)
this.setHighlightedAriaSnapshot(data.params.ariaSnapshot); this.setHighlightedAriaTemplate(data.params.ariaTemplate);
return; return;
} }
if (data.event === 'step') { if (data.event === 'step') {
@ -169,7 +169,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
mode: this._mode, mode: this._mode,
actionPoint, actionPoint,
actionSelector, actionSelector,
ariaTemplate: this._highlightedElement.ariaSnapshot, ariaTemplate: this._highlightedElement.ariaTemplate,
language: this._currentLanguage, language: this._currentLanguage,
testIdAttributeName: this._contextRecorder.testIdAttributeName(), testIdAttributeName: this._contextRecorder.testIdAttributeName(),
overlay: this._overlayState, overlay: this._overlayState,
@ -245,8 +245,8 @@ export class Recorder implements InstrumentationListener, IRecorder {
this._refreshOverlay(); this._refreshOverlay();
} }
setHighlightedAriaSnapshot(ariaSnapshot: ParsedYaml) { setHighlightedAriaTemplate(ariaTemplate: ParsedYaml) {
this._highlightedElement = { ariaSnapshot }; this._highlightedElement = { ariaTemplate };
this._refreshOverlay(); this._refreshOverlay();
} }

View file

@ -38,7 +38,7 @@ export class RecorderCollection extends EventEmitter {
restart() { restart() {
this._actions = []; this._actions = [];
this._fireChange(); this.emit('change', []);
} }
setEnabled(enabled: boolean) { setEnabled(enabled: boolean) {
@ -128,6 +128,7 @@ export class RecorderCollection extends EventEmitter {
private _fireChange() { private _fireChange() {
if (!this._enabled) if (!this._enabled)
return; return;
this.emit('change', collapseActions(this._actions)); this.emit('change', collapseActions(this._actions));
} }
} }

View file

@ -38,8 +38,6 @@ const BIN_PATH = path.join(__dirname, '..', '..', '..', 'bin');
const PLAYWRIGHT_CDN_MIRRORS = [ const PLAYWRIGHT_CDN_MIRRORS = [
'https://playwright.azureedge.net', 'https://playwright.azureedge.net',
'https://playwright-akamai.azureedge.net',
'https://playwright-verizon.azureedge.net',
]; ];
if (process.env.PW_TEST_CDN_THAT_SHOULD_WORK) { if (process.env.PW_TEST_CDN_THAT_SHOULD_WORK) {
@ -79,7 +77,7 @@ const EXECUTABLE_PATHS = {
}; };
type DownloadPaths = Record<HostPlatform, string | undefined>; type DownloadPaths = Record<HostPlatform, string | undefined>;
const DOWNLOAD_PATHS: Record<BrowserName | InternalTool | 'chromium-headless-shell', DownloadPaths> = { const DOWNLOAD_PATHS: Record<BrowserName | InternalTool, DownloadPaths> = {
'chromium': { 'chromium': {
'<unknown>': undefined, '<unknown>': undefined,
'ubuntu18.04-x64': undefined, 'ubuntu18.04-x64': undefined,
@ -403,9 +401,9 @@ function readDescriptors(browsersJSON: BrowsersJSON): BrowsersJSONDescriptor[] {
} }
export type BrowserName = 'chromium' | 'firefox' | 'webkit' | 'bidi'; 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 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']; const allDownloadable = ['android', 'chromium', 'firefox', 'webkit', 'ffmpeg', 'firefox-beta', 'chromium-tip-of-tree', 'chromium-headless-shell'];
export interface Executable { export interface Executable {
@ -488,21 +486,6 @@ export class Registry {
_dependencyGroup: 'chromium', _dependencyGroup: 'chromium',
_isHermeticInstallation: true, _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 chromiumHeadlessShell = descriptors.find(d => d.name === 'chromium-headless-shell')!;
const chromiumHeadlessShellExecutable = findExecutablePath(chromiumHeadlessShell.dir, 'chromium-headless-shell'); const chromiumHeadlessShellExecutable = findExecutablePath(chromiumHeadlessShell.dir, 'chromium-headless-shell');
@ -512,7 +495,7 @@ export class Registry {
browserName: 'chromium', browserName: 'chromium',
directory: chromiumHeadlessShell.dir, directory: chromiumHeadlessShell.dir,
executablePath: () => chromiumHeadlessShellExecutable, 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', installType: chromiumHeadlessShell.installByDefault ? 'download-by-default' : 'download-on-demand',
_validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, chromiumHeadlessShell.dir, ['chrome-linux'], [], ['chrome-win']), _validateHostRequirements: (sdkLanguage: string) => this._validateHostRequirements(sdkLanguage, chromiumHeadlessShell.dir, ['chrome-linux'], [], ['chrome-win']),
downloadURLs: this._downloadURLs(chromiumHeadlessShell), downloadURLs: this._downloadURLs(chromiumHeadlessShell),
@ -894,16 +877,8 @@ export class Registry {
return this._executables.filter(e => e.installType === 'download-by-default'); return this._executables.filter(e => e.installType === 'download-by-default');
} }
private _addRequirementsAndDedupe(executables: Executable[]): ExecutableImpl[] { private _dedupe(executables: Executable[]): ExecutableImpl[] {
const set = new Set<ExecutableImpl>(); return Array.from(new Set(executables as 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 async _validateHostRequirements(sdkLanguage: string, browserDirectory: string, linuxLddDirectories: string[], dlOpenLibraries: string[], windowsExeAndDllDirectories: string[]) { private async _validateHostRequirements(sdkLanguage: string, browserDirectory: string, linuxLddDirectories: string[], dlOpenLibraries: string[], windowsExeAndDllDirectories: string[]) {
@ -914,7 +889,7 @@ export class Registry {
} }
async installDeps(executablesToInstallDeps: Executable[], dryRun: boolean) { async installDeps(executablesToInstallDeps: Executable[], dryRun: boolean) {
const executables = this._addRequirementsAndDedupe(executablesToInstallDeps); const executables = this._dedupe(executablesToInstallDeps);
const targets = new Set<DependencyGroup>(); const targets = new Set<DependencyGroup>();
for (const executable of executables) { for (const executable of executables) {
if (executable._dependencyGroup) if (executable._dependencyGroup)
@ -928,7 +903,7 @@ export class Registry {
} }
async install(executablesToInstall: Executable[], forceReinstall: boolean) { async install(executablesToInstall: Executable[], forceReinstall: boolean) {
const executables = this._addRequirementsAndDedupe(executablesToInstall); const executables = this._dedupe(executablesToInstall);
await fs.promises.mkdir(registryDirectory, { recursive: true }); await fs.promises.mkdir(registryDirectory, { recursive: true });
const lockfilePath = path.join(registryDirectory, '__dirlock'); const lockfilePath = path.join(registryDirectory, '__dirlock');
const linksDir = path.join(registryDirectory, '.links'); const linksDir = path.join(registryDirectory, '.links');
@ -1224,11 +1199,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[]) { export async function installBrowsersForNpmInstall(browsers: string[]) {
// PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD should have a value of 0 or 1 // PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD should have a value of 0 or 1
if (getAsBooleanFromENV('PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD')) { if (getAsBooleanFromENV('PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD')) {

View file

@ -47,6 +47,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
const kTargetAttribute = '__playwright_target__'; const kTargetAttribute = '__playwright_target__';
const kCustomElementsAttribute = '__playwright_custom_elements__'; const kCustomElementsAttribute = '__playwright_custom_elements__';
const kCurrentSrcAttribute = '__playwright_current_src__'; const kCurrentSrcAttribute = '__playwright_current_src__';
const kBoundingRectAttribute = '__playwright_bounding_rect__';
// Symbols for our own info on Nodes/StyleSheets. // Symbols for our own info on Nodes/StyleSheets.
const kSnapshotFrameId = Symbol('__playwright_snapshot_frameid_'); const kSnapshotFrameId = Symbol('__playwright_snapshot_frameid_');
@ -436,6 +437,18 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
expectValue(value); expectValue(value);
attrs[kSelectedAttribute] = 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) { if (element.scrollTop) {
expectValue(kScrollTopAttribute); expectValue(kScrollTopAttribute);
expectValue(element.scrollTop); expectValue(element.scrollTop);

View file

@ -612,6 +612,9 @@ export class WKPage implements PageDelegate {
event.type as dialog.DialogType, event.type as dialog.DialogType,
event.message, event.message,
async (accept: boolean, promptText?: string) => { async (accept: boolean, promptText?: string) => {
// TODO: this should actually be a RDP event that notifies about a cancelled navigation attempt.
if (event.type === 'beforeunload' && !accept)
this._page._frameManager.frameAbortedNavigation(this._page.mainFrame()._id, 'navigation cancelled by beforeunload dialog');
await this._pageProxySession.send('Dialog.handleJavaScriptDialog', { accept, promptText }); await this._pageProxySession.send('Dialog.handleJavaScriptDialog', { accept, promptText });
}, },
event.defaultPrompt)); event.defaultPrompt));

View file

@ -14709,9 +14709,12 @@ export interface BrowserType<Unused = {}> {
bypassCSP?: boolean; bypassCSP?: boolean;
/** /**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", * Browser distribution channel.
* "msedge", "msedge-beta", "msedge-dev", "msedge-canary". Read more about using *
* [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge). * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#opt-in-to-new-headless-mode).
*
* Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or
* "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge).
*/ */
channel?: string; channel?: string;
@ -15205,9 +15208,12 @@ export interface BrowserType<Unused = {}> {
args?: Array<string>; args?: Array<string>;
/** /**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", * Browser distribution channel.
* "msedge", "msedge-beta", "msedge-dev", "msedge-canary". Read more about using *
* [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge). * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#opt-in-to-new-headless-mode).
*
* Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or
* "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge).
*/ */
channel?: string; channel?: string;
@ -21540,9 +21546,12 @@ export interface LaunchOptions {
args?: Array<string>; args?: Array<string>;
/** /**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", * Browser distribution channel.
* "msedge", "msedge-beta", "msedge-dev", "msedge-canary". Read more about using *
* [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge). * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#opt-in-to-new-headless-mode).
*
* Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or
* "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge).
*/ */
channel?: string; channel?: string;

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-core", "name": "@playwright/experimental-ct-core",
"version": "1.49.0-next", "version": "1.49.1",
"description": "Playwright Component Testing Helpers", "description": "Playwright Component Testing Helpers",
"repository": { "repository": {
"type": "git", "type": "git",
@ -26,8 +26,8 @@
} }
}, },
"dependencies": { "dependencies": {
"playwright-core": "1.49.0-next", "playwright-core": "1.49.1",
"vite": "^5.2.8", "vite": "^5.2.8",
"playwright": "1.49.0-next" "playwright": "1.49.1"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-react", "name": "@playwright/experimental-ct-react",
"version": "1.49.0-next", "version": "1.49.1",
"description": "Playwright Component Testing for React", "description": "Playwright Component Testing for React",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next", "@playwright/experimental-ct-core": "1.49.1",
"@vitejs/plugin-react": "^4.2.1" "@vitejs/plugin-react": "^4.2.1"
}, },
"bin": { "bin": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-react17", "name": "@playwright/experimental-ct-react17",
"version": "1.49.0-next", "version": "1.49.1",
"description": "Playwright Component Testing for React", "description": "Playwright Component Testing for React",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next", "@playwright/experimental-ct-core": "1.49.1",
"@vitejs/plugin-react": "^4.2.1" "@vitejs/plugin-react": "^4.2.1"
}, },
"bin": { "bin": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-svelte", "name": "@playwright/experimental-ct-svelte",
"version": "1.49.0-next", "version": "1.49.1",
"description": "Playwright Component Testing for Svelte", "description": "Playwright Component Testing for Svelte",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next", "@playwright/experimental-ct-core": "1.49.1",
"@sveltejs/vite-plugin-svelte": "^3.0.1" "@sveltejs/vite-plugin-svelte": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-vue", "name": "@playwright/experimental-ct-vue",
"version": "1.49.0-next", "version": "1.49.1",
"description": "Playwright Component Testing for Vue", "description": "Playwright Component Testing for Vue",
"repository": { "repository": {
"type": "git", "type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.49.0-next", "@playwright/experimental-ct-core": "1.49.1",
"@vitejs/plugin-vue": "^4.2.1" "@vitejs/plugin-vue": "^4.2.1"
}, },
"bin": { "bin": {

View file

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

View file

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

View file

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

View file

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

View file

@ -181,7 +181,7 @@ export function toHaveAccessibleDescription(
options?: { timeout?: number, ignoreCase?: boolean }, options?: { timeout?: number, ignoreCase?: boolean },
) { ) {
return toMatchText.call(this, 'toHaveAccessibleDescription', locator, 'Locator', async (isNot, timeout) => { 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 }); return await locator._expect('to.have.accessible.description', { expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} }
@ -193,7 +193,7 @@ export function toHaveAccessibleName(
options?: { timeout?: number, ignoreCase?: boolean }, options?: { timeout?: number, ignoreCase?: boolean },
) { ) {
return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => { 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 }); return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} }

View file

@ -18,7 +18,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import type { ReporterDescription } from '../../types/test'; import type { ReporterDescription } from '../../types/test';
import type { FullConfigInternal } from '../common/config'; 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 { TeleReporterReceiver } from '../isomorphic/teleReceiver';
import { JsonStringInternalizer, StringInternPool } from '../isomorphic/stringInternPool'; import { JsonStringInternalizer, StringInternPool } from '../isomorphic/stringInternPool';
import { createReporters } from '../runner/reporters'; import { createReporters } from '../runner/reporters';
@ -471,7 +471,7 @@ class PathSeparatorPatcher {
} }
if (jsonEvent.method === 'onTestEnd') { if (jsonEvent.method === 'onTestEnd') {
const testResult = jsonEvent.params.result as JsonTestResultEnd; 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 => { testResult.attachments.forEach(attachment => {
if (attachment.path) if (attachment.path)
attachment.path = this._updatePath(attachment.path); attachment.path = this._updatePath(attachment.path);
@ -483,6 +483,11 @@ class PathSeparatorPatcher {
this._updateLocation(step.location); this._updateLocation(step.location);
return; return;
} }
if (jsonEvent.method === 'onStepEnd') {
const step = jsonEvent.params.step as JsonTestStepEnd;
this._updateErrorLocations(step.error);
return;
}
} }
private _updateProject(project: JsonProject) { 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) { private _updateLocation(location?: JsonLocation) {
if (location) if (location)
location.file = this._updatePath(location.file); location.file = this._updatePath(location.file);

View file

@ -83,6 +83,10 @@ export async function applySuggestedRebaselines(config: FullConfigInternal, repo
const indent = lines[matcher.loc!.start.line - 1].match(/^\s*/)![0]; const indent = lines[matcher.loc!.start.line - 1].match(/^\s*/)![0];
const newText = replacement.code.replace(/\{indent\}/g, indent); const newText = replacement.code.replace(/\{indent\}/g, indent);
ranges.push({ start: matcher.start!, end: node.end!, oldText: source.substring(matcher.start!, node.end!), newText }); ranges.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;
} }
} }
}); });

View file

@ -5776,9 +5776,12 @@ export interface PlaywrightWorkerOptions {
*/ */
headless: boolean; headless: boolean;
/** /**
* Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", * Browser distribution channel.
* "msedge", "msedge-beta", "msedge-dev", "msedge-canary". Read more about using *
* [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge). * Use "chromium" to [opt in to new headless mode](https://playwright.dev/docs/browsers#opt-in-to-new-headless-mode).
*
* Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or
* "msedge-canary" to use branded [Google Chrome and Microsoft Edge](https://playwright.dev/docs/browsers#google-chrome--microsoft-edge).
* *
* **Usage** * **Usage**
* *

View file

@ -741,10 +741,12 @@ export type DebugControllerSetRecorderModeOptions = {
}; };
export type DebugControllerSetRecorderModeResult = void; export type DebugControllerSetRecorderModeResult = void;
export type DebugControllerHighlightParams = { export type DebugControllerHighlightParams = {
selector: string, selector?: string,
ariaTemplate?: string,
}; };
export type DebugControllerHighlightOptions = { export type DebugControllerHighlightOptions = {
selector?: string,
ariaTemplate?: string,
}; };
export type DebugControllerHighlightResult = void; export type DebugControllerHighlightResult = void;
export type DebugControllerHideHighlightParams = {}; export type DebugControllerHideHighlightParams = {};

View file

@ -791,7 +791,8 @@ DebugController:
highlight: highlight:
parameters: parameters:
selector: string selector: string?
ariaTemplate: string?
hideHighlight: hideHighlight:

View file

@ -27,7 +27,7 @@ import { CallLogView } from './callLog';
import './recorder.css'; import './recorder.css';
import { asLocator } from '@isomorphic/locatorGenerators'; import { asLocator } from '@isomorphic/locatorGenerators';
import { toggleTheme } from '@web/theme'; import { toggleTheme } from '@web/theme';
import { copy } from '@web/uiUtils'; import { copy, useSetting } from '@web/uiUtils';
import yaml from 'yaml'; import yaml from 'yaml';
import { parseAriaKey } from '@isomorphic/ariaSnapshot'; import { parseAriaKey } from '@isomorphic/ariaSnapshot';
import type { AriaKeyError, ParsedYaml } 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 [selectedFileId, setSelectedFileId] = React.useState<string | undefined>();
const [runningFileId, setRunningFileId] = 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 [ariaSnapshot, setAriaSnapshot] = React.useState<string | undefined>();
const [ariaSnapshotErrors, setAriaSnapshotErrors] = React.useState<SourceHighlight[]>(); const [ariaSnapshotErrors, setAriaSnapshotErrors] = React.useState<SourceHighlight[]>();
@ -67,6 +67,7 @@ export const Recorder: React.FC<RecorderProps> = ({
const language = source.language; const language = source.language;
setLocator(asLocator(language, elementInfo.selector)); setLocator(asLocator(language, elementInfo.selector));
setAriaSnapshot(elementInfo.ariaSnapshot); setAriaSnapshot(elementInfo.ariaSnapshot);
setAriaSnapshotErrors([]);
if (userGesture && selectedTab !== 'locator' && selectedTab !== 'aria') if (userGesture && selectedTab !== 'locator' && selectedTab !== 'aria')
setSelectedTab('locator'); setSelectedTab('locator');
@ -120,11 +121,8 @@ export const Recorder: React.FC<RecorderProps> = ({
setAriaSnapshotErrors(errors); setAriaSnapshotErrors(errors);
setAriaSnapshot(ariaSnapshot); setAriaSnapshot(ariaSnapshot);
if (!errors.length) if (!errors.length)
window.dispatch({ event: 'highlightRequested', params: { ariaSnapshot: fragment } }); window.dispatch({ event: 'highlightRequested', params: { ariaTemplate: fragment } });
}, [mode]); }, [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'> return <div className='recorder'>
<Toolbar> <Toolbar>
@ -191,7 +189,7 @@ export const Recorder: React.FC<RecorderProps> = ({
{ {
id: 'locator', id: 'locator',
title: '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', id: 'log',
@ -200,8 +198,8 @@ export const Recorder: React.FC<RecorderProps> = ({
}, },
{ {
id: 'aria', id: 'aria',
title: 'Aria snapshot', title: 'Aria',
render: () => <CodeMirrorWrapper text={ariaPlaceholder || ariaSnapshot || ''} language={'yaml'} readOnly={isRecording} onChange={onAriaEditorChange} highlight={ariaSnapshotErrors} wrapLines={true} /> render: () => <CodeMirrorWrapper text={ariaSnapshot || ''} placeholder='Type aria template to match' language={'yaml'} onChange={onAriaEditorChange} highlight={ariaSnapshotErrors} wrapLines={true} />
}, },
]} ]}
selectedTab={selectedTab} selectedTab={selectedTab}

View file

@ -427,14 +427,20 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
for (const canvas of canvasElements) { for (const canvas of canvasElements) {
const context = canvas.getContext('2d')!; const context = canvas.getContext('2d')!;
const boundingRect = canvas.getBoundingClientRect(); const boundingRectAttribute = canvas.getAttribute('__playwright_bounding_rect__');
const xStart = boundingRect.left / window.innerWidth; canvas.removeAttribute('__playwright_bounding_rect__');
const yStart = boundingRect.top / window.innerHeight; if (!boundingRectAttribute)
const xEnd = boundingRect.right / window.innerWidth; continue;
const yEnd = boundingRect.bottom / window.innerHeight;
const partiallyUncaptured = xEnd > 1 || yEnd > 1; let boundingRect: { left: number, top: number, right: number, bottom: number };
const fullyUncaptured = xStart > 1 || yStart > 1; 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) { if (fullyUncaptured) {
canvas.title = `Playwright couldn't capture canvas contents because it's located outside the viewport.`; canvas.title = `Playwright couldn't capture canvas contents because it's located outside the viewport.`;
continue; continue;
@ -442,10 +448,10 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
drawCheckerboard(context, canvas); 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) if (isUnderTest)
// eslint-disable-next-line no-console // 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) if (partiallyUncaptured)
canvas.title = `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`; canvas.title = `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`;

View file

@ -24,6 +24,7 @@ import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python'; import 'codemirror/mode/python/python';
import 'codemirror/mode/clike/clike'; import 'codemirror/mode/clike/clike';
import 'codemirror/mode/markdown/markdown'; import 'codemirror/mode/markdown/markdown';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/mode/simple'; import 'codemirror/addon/mode/simple';
import 'codemirror/mode/yaml/yaml'; import 'codemirror/mode/yaml/yaml';

View file

@ -181,3 +181,7 @@ body.dark-mode .CodeMirror span.cm-type {
text-decoration-color: var(--vscode-errorForeground); text-decoration-color: var(--vscode-errorForeground);
text-decoration-style: wavy; text-decoration-style: wavy;
} }
.CodeMirror-placeholder {
color: var(--vscode-input-placeholderForeground) !important;
}

View file

@ -46,6 +46,7 @@ export interface SourceProps {
wrapLines?: boolean; wrapLines?: boolean;
onChange?: (text: string) => void; onChange?: (text: string) => void;
dataTestId?: string; dataTestId?: string;
placeholder?: string;
} }
export const CodeMirrorWrapper: React.FC<SourceProps> = ({ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
@ -62,6 +63,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
wrapLines, wrapLines,
onChange, onChange,
dataTestId, dataTestId,
placeholder,
}) => { }) => {
const [measure, codemirrorElement] = useMeasure<HTMLDivElement>(); const [measure, codemirrorElement] = useMeasure<HTMLDivElement>();
const [modulePromise] = React.useState<Promise<CodeMirror>>(import('./codeMirrorModule').then(m => m.default)); 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') && mode === codemirrorRef.current.cm.getOption('mode')
&& !!readOnly === codemirrorRef.current.cm.getOption('readOnly') && !!readOnly === codemirrorRef.current.cm.getOption('readOnly')
&& lineNumbers === codemirrorRef.current.cm.getOption('lineNumbers') && 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. // No need to re-create codemirror.
return; return;
} }
@ -102,6 +105,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
readOnly: !!readOnly, readOnly: !!readOnly,
lineNumbers, lineNumbers,
lineWrapping: wrapLines, lineWrapping: wrapLines,
placeholder,
}); });
codemirrorRef.current = { cm }; codemirrorRef.current = { cm };
if (isFocused) if (isFocused)
@ -109,7 +113,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
setCodemirror(cm); setCodemirror(cm);
return 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(() => { React.useEffect(() => {
if (codemirrorRef.current) if (codemirrorRef.current)

View file

@ -319,7 +319,9 @@ function indexTree<T extends TreeItem>(
selectedItem: T | undefined, selectedItem: T | undefined,
expandedItems: Map<string, boolean | undefined>, expandedItems: Map<string, boolean | undefined>,
autoExpandDepth: number, autoExpandDepth: number,
isVisible?: (item: T) => boolean): Map<T, TreeItemData> { isVisible: (item: T) => boolean = () => true): Map<T, TreeItemData> {
if (!isVisible(rootItem))
return new Map();
const result = new Map<T, TreeItemData>(); const result = new Map<T, TreeItemData>();
const temporaryExpanded = new Set<string>(); const temporaryExpanded = new Set<string>();
@ -328,9 +330,9 @@ function indexTree<T extends TreeItem>(
let lastItem: T | null = null; let lastItem: T | null = null;
const appendChildren = (parent: T, depth: number) => { const appendChildren = (parent: T, depth: number) => {
if (isVisible && !isVisible(parent))
return;
for (const item of parent.children as T[]) { for (const item of parent.children as T[]) {
if (!isVisible(item))
continue;
const expandState = temporaryExpanded.has(item.id) || expandedItems.get(item.id); const expandState = temporaryExpanded.has(item.id) || expandedItems.get(item.id);
const autoExpandMatches = autoExpandDepth > depth && result.size < 25 && expandState !== false; const autoExpandMatches = autoExpandDepth > depth && result.size < 25 && expandState !== false;
const expanded = item.children.length ? expandState ?? autoExpandMatches : undefined; const expanded = item.children.length ? expandState ?? autoExpandMatches : undefined;

View file

@ -14,6 +14,7 @@
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.1.0", "@vitejs/plugin-vue": "^4.1.0",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"typescript": "5.6.2",
"vite": "^5.2.8", "vite": "^5.2.8",
"vue-tsc": "^2.0.21" "vue-tsc": "^2.0.21"
} }

View file

@ -35,6 +35,7 @@ export type BrowserTestWorkerFixtures = PageWorkerFixtures & {
browserType: BrowserType; browserType: BrowserType;
isAndroid: boolean; isAndroid: boolean;
isElectron: boolean; isElectron: boolean;
isHeadlessShell: boolean;
nodeVersion: { major: number, minor: number, patch: number }; nodeVersion: { major: number, minor: number, patch: number };
bidiTestSkipPredicate: (info: TestInfo) => boolean; bidiTestSkipPredicate: (info: TestInfo) => boolean;
}; };
@ -97,6 +98,10 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
electronMajorVersion: [0, { scope: 'worker' }], electronMajorVersion: [0, { scope: 'worker' }],
isWebView2: [false, { 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) => { contextFactory: async ({ _contextFactory }: any, run) => {
await run(_contextFactory); await run(_contextFactory);
}, },

View file

@ -20,8 +20,6 @@ import type { AddressInfo } from 'net';
const CDNS = [ const CDNS = [
'https://playwright.azureedge.net', 'https://playwright.azureedge.net',
'https://playwright-akamai.azureedge.net',
'https://playwright-verizon.azureedge.net',
]; ];
const DL_STAT_BLOCK = /^.*from url: (.*)$\n^.*to location: (.*)$\n^.*response status code: (.*)$\n^.*total bytes: (\d+)$\n^.*download complete, size: (\d+)$\n^.*SUCCESS downloading (\w+) .*$/gm; const DL_STAT_BLOCK = /^.*from url: (.*)$\n^.*to location: (.*)$\n^.*response status code: (.*)$\n^.*total bytes: (\d+)$\n^.*download complete, size: (\d+)$\n^.*SUCCESS downloading (\w+) .*$/gm;

View file

@ -76,15 +76,24 @@ test(`playwright should work`, async ({ exec, installedSoftwareOnDisk }) => {
await exec('node esm-playwright.mjs'); 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'); const result1 = await exec('npm i --foreground-scripts playwright');
expect(result1).toHaveLoggedSoftwareDownload([]); expect(result1).toHaveLoggedSoftwareDownload([]);
expect(await installedSoftwareOnDisk()).toEqual([]); 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(result2).toHaveLoggedSoftwareDownload(['chromium', 'ffmpeg']);
expect(await installedSoftwareOnDisk()).toEqual(['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 }) => { test('@playwright/test should work', async ({ exec, installedSoftwareOnDisk }) => {
const result1 = await exec('npm i --foreground-scripts @playwright/test'); const result1 = await exec('npm i --foreground-scripts @playwright/test');
expect(result1).toHaveLoggedSoftwareDownload([]); expect(result1).toHaveLoggedSoftwareDownload([]);

View file

@ -104,3 +104,25 @@ it('should not stall on evaluate when dismissing beforeunload', async ({ page, s
]); ]);
}); });
it('should not stall on click when dismissing beforeunload', async ({ page, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33806' });
await page.goto(server.EMPTY_PAGE);
await page.setContent(`<a href="${server.PREFIX}/frames/one-frame.html">click me</a>`);
await page.evaluate(() => {
window.onbeforeunload = () => false;
});
page.on('dialog', async dialog => {
await dialog.dismiss();
});
await page.getByRole('link').click({ noWaitAfter: true });
await page.evaluate(() => {
window.onbeforeunload = null;
});
// This line should not timeout.
await page.getByRole('link').click({ timeout: 5000 });
await expect(page).toHaveURL(server.PREFIX + '/frames/one-frame.html');
});

View file

@ -18,8 +18,7 @@
import { browserTest as base, expect } from '../config/browserTest'; import { browserTest as base, expect } from '../config/browserTest';
const it = base.extend<{ failsOn401: boolean }>({ const it = base.extend<{ failsOn401: boolean }>({
failsOn401: async ({ browserName, headless, channel }, use) => { failsOn401: async ({ browserName, isHeadlessShell }, use) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
await use(browserName === 'chromium' && !isHeadlessShell); await use(browserName === 'chromium' && !isHeadlessShell);
}, },
}); });

View file

@ -52,8 +52,7 @@ it('should open devtools when "devtools: true" option is given', async ({ browse
await browser.close(); await browser.close();
}); });
it('should return background pages', async ({ browserType, createUserDataDir, asset, headless, channel }) => { it('should return background pages', async ({ browserType, createUserDataDir, asset, isHeadlessShell }) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions'); it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
const userDataDir = await createUserDataDir(); const userDataDir = await createUserDataDir();
@ -78,8 +77,7 @@ it('should return background pages', async ({ browserType, createUserDataDir, as
expect(context.backgroundPages().length).toBe(0); expect(context.backgroundPages().length).toBe(0);
}); });
it('should return background pages when recording video', async ({ browserType, createUserDataDir, asset, headless, channel }, testInfo) => { it('should return background pages when recording video', async ({ browserType, createUserDataDir, asset, isHeadlessShell }, testInfo) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions'); it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
const userDataDir = await createUserDataDir(); const userDataDir = await createUserDataDir();
@ -105,8 +103,7 @@ it('should return background pages when recording video', async ({ browserType,
await context.close(); await context.close();
}); });
it('should support request/response events when using backgroundPage()', async ({ browserType, createUserDataDir, asset, server, headless, channel }) => { it('should support request/response events when using backgroundPage()', async ({ browserType, createUserDataDir, asset, server, isHeadlessShell }) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions'); it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
server.setRoute('/empty.html', (req, res) => { 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', { it('should report console messages from content script', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32762' } annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32762' }
}, async ({ browserType, createUserDataDir, asset, server, headless, channel }) => { }, async ({ browserType, createUserDataDir, asset, server, isHeadlessShell }) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
it.skip(isHeadlessShell, 'Headless Shell has no support for extensions'); it.skip(isHeadlessShell, 'Headless Shell has no support for extensions');
const userDataDir = await createUserDataDir(); const userDataDir = await createUserDataDir();

View file

@ -20,6 +20,7 @@ import { createGuid } from '../../packages/playwright-core/lib/utils/crypto';
import { Backend } from '../config/debugControllerBackend'; import { Backend } from '../config/debugControllerBackend';
import type { Browser, BrowserContext } from '@playwright/test'; import type { Browser, BrowserContext } from '@playwright/test';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { roundBox } from '../page/pageTest';
type BrowserWithReuse = Browser & { _newContextForReuse: () => Promise<BrowserContext> }; type BrowserWithReuse = Browser & { _newContextForReuse: () => Promise<BrowserContext> };
type Fixtures = { type Fixtures = {
@ -30,7 +31,8 @@ type Fixtures = {
}; };
const test = baseTest.extend<Fixtures>({ const test = baseTest.extend<Fixtures>({
wsEndpoint: async ({ }, use) => { wsEndpoint: async ({ headless }, use) => {
if (headless)
process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1'; process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1';
const server = new PlaywrightServer({ mode: 'extension', path: '/' + createGuid(), maxConnections: Number.MAX_VALUE, enableSocksProxy: false }); const server = new PlaywrightServer({ mode: 'extension', path: '/' + createGuid(), maxConnections: Number.MAX_VALUE, enableSocksProxy: false });
const wsEndpoint = await server.listen(); const wsEndpoint = await server.listen();
@ -279,3 +281,20 @@ test('should highlight inside iframe', async ({ backend, connectedBrowser }, tes
await expect(highlight).toHaveCount(1); await expect(highlight).toHaveCount(1);
await expect(page.locator('x-pw-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);
});

View file

@ -636,8 +636,7 @@ it('should be able to download a inline PDF file via response interception', asy
await page.close(); await page.close();
}); });
it('should be able to download a inline PDF file via navigation', async ({ browser, server, asset, browserName, channel, headless }) => { it('should be able to download a inline PDF file via navigation', async ({ browser, server, asset, browserName, isHeadlessShell }) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
it.skip(browserName === 'chromium' && !isHeadlessShell, 'We expect PDF Viewer to open up in headed Chromium'); it.skip(browserName === 'chromium' && !isHeadlessShell, 'We expect PDF Viewer to open up in headed Chromium');
const page = await browser.newPage(); const page = await browser.newPage();

View file

@ -101,10 +101,9 @@ it('should change document.activeElement', async ({ page, server }) => {
expect(active).toEqual(['INPUT', 'TEXTAREA']); 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 === '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'); 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'); it.fixme(browserName === 'chromium' && !isHeadlessShell, 'https://github.com/microsoft/playwright/issues/33330');
const page2 = await page.context().newPage(); const page2 = await page.context().newPage();

View file

@ -926,4 +926,34 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`)
const predicate = (msg: ConsoleMessage) => msg.type() === 'error' && /Content[\- ]Security[\- ]Policy/i.test(msg.text()); const predicate = (msg: ConsoleMessage) => msg.type() === 'error' && /Content[\- ]Security[\- ]Policy/i.test(msg.text());
await expect(page.waitForEvent('console', { predicate, timeout: 1000 })).rejects.toThrow(); await expect(page.waitForEvent('console', { predicate, timeout: 1000 })).rejects.toThrow();
}); });
test('should clear when recording is disabled', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33802' } }, async ({ openRecorder }) => {
const { recorder } = await openRecorder();
await recorder.setContentAndWait(`
<button id="foo" onclick="console.log('click')">Foo</button>
<button id="bar" onclick="console.log('click')">Bar</button>
`);
await recorder.hoverOverElement('#foo');
let [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'click'),
recorder.trustedClick(),
]);
expect(sources.get('JavaScript').text).toContain(`getByRole('button', { name: 'Foo' }).click()`);
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.recorderPage.getByRole('button', { name: 'Clear' }).click();
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
await recorder.hoverOverElement('#bar');
[sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'click'),
recorder.trustedClick(),
]);
expect(sources.get('JavaScript').text).toContain(`getByRole('button', { name: 'Bar' }).click()`);
expect(sources.get('JavaScript').text).not.toContain(`getByRole('button', { name: 'Foo' })`);
});
}); });

View file

@ -402,17 +402,6 @@ await page1.GotoAsync("about:blank?foo");`);
await expect.poll(() => messages).toEqual(['mousedown', 'mouseup', 'click']); 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 }) => { test('should reset hover model on action when element detaches', async ({ openRecorder }) => {
const { page, recorder } = await openRecorder(); const { page, recorder } = await openRecorder();

View file

@ -64,11 +64,10 @@ test.describe(() => {
test('should inspect aria snapshot', async ({ openRecorder }) => { test('should inspect aria snapshot', async ({ openRecorder }) => {
const { recorder } = await openRecorder(); const { recorder } = await openRecorder();
await recorder.setContentAndWait(`<main><button>Submit</button></main>`); 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.click('x-pw-tool-item.pick-locator');
await recorder.page.hover('button'); await recorder.page.hover('button');
await recorder.trustedClick(); 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(` await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- textbox - textbox
- text: '- button "Submit"' - text: '- button "Submit"'
@ -85,12 +84,11 @@ test.describe(() => {
const submitButton = recorder.page.getByRole('button', { name: 'Submit' }); const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
const cancelButton = recorder.page.getByRole('button', { name: 'Cancel' }); 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 recorder.page.click('x-pw-tool-item.pick-locator');
await submitButton.hover(); await submitButton.hover();
await recorder.trustedClick(); 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(` await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- text: '- button "Submit"' - text: '- button "Submit"'
`); `);
@ -128,13 +126,12 @@ test.describe(() => {
</main>`); </main>`);
const submitButton = recorder.page.getByRole('button', { name: 'Submit' }); 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 recorder.page.click('x-pw-tool-item.pick-locator');
await submitButton.hover(); await submitButton.hover();
await recorder.trustedClick(); 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(` await expect(recorder.recorderPage.locator('.tab-aria .CodeMirror')).toMatchAriaSnapshot(`
- text: '- button "Submit"' - text: '- button "Submit"'
`); `);

View 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);
}
});
});

View file

@ -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.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.fail(browserName === 'firefox', 'No such permissions (requires flag) in Firefox');
it.fixme(browserName === 'webkit' && isWindows, 'WebPasteboardProxy::allPasteboardItemInfo not implemented for Windows.'); 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') if (browserName !== 'webkit')
expect(await getPermission(page, 'clipboard-read')).toBe('prompt'); expect(await getPermission(page, 'clipboard-read')).toBe('prompt');
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless); if (isHeadlessShell) {
if (browserName === 'chromium' && isHeadlessShell) {
// Chromium (but not headless-shell) shows a dialog and does not resolve the promise. // 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); const error = await page.evaluate(() => navigator.clipboard.readText()).catch(e => e);
expect(error.toString()).toContain('denied'); expect(error.toString()).toContain('denied');

View file

@ -495,6 +495,16 @@ test('should not include hidden pseudo into accessible name', async ({ page }) =
expect.soft(await getNameAndRole(page, 'a')).toEqual({ role: 'link', name: 'hello hello' }); expect.soft(await getNameAndRole(page, 'a')).toEqual({ role: 'link', name: 'hello hello' });
}); });
test('should ignore invalid aria-labelledby', async ({ page }) => {
await page.setContent(`
<label>
<span>Text here</span>
<input type=text aria-labelledby="does-not-exist">
</label>
`);
expect.soft(await getNameAndRole(page, 'input')).toEqual({ role: 'textbox', name: 'Text here' });
});
function toArray(x: any): any[] { function toArray(x: any): any[] {
return Array.isArray(x) ? x : [x]; return Array.isArray(x) ? x : [x];
} }

View file

@ -22,8 +22,7 @@ import { verifyViewport } from '../config/utils';
browserTest.describe('page screenshot', () => { browserTest.describe('page screenshot', () => {
browserTest.skip(({ browserName, headless }) => browserName === 'firefox' && !headless, 'Firefox headed produces a different image.'); 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 }) => { browserTest('should run in parallel in multiple pages', async ({ server, contextFactory, browserName, isHeadlessShell }) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
browserTest.fixme(browserName === 'chromium' && !isHeadlessShell, 'https://github.com/microsoft/playwright/issues/33330'); browserTest.fixme(browserName === 'chromium' && !isHeadlessShell, 'https://github.com/microsoft/playwright/issues/33330');
const context = await contextFactory(); const context = await contextFactory();

View file

@ -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:') }); 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'); 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.`); await expect(snapshot.locator('canvas')).toHaveAttribute('title', `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`);

View file

@ -429,8 +429,7 @@ for (const params of [
height: 768, height: 768,
} }
]) { ]) {
browserTest(`should produce screencast frames ${params.id}`, async ({ video, contextFactory, browserName, platform, headless, channel }, testInfo) => { browserTest(`should produce screencast frames ${params.id}`, async ({ video, contextFactory, browserName, platform, headless, isHeadlessShell }, testInfo) => {
const isHeadlessShell = channel === 'chromium-headless-shell' || (!channel && headless);
browserTest.skip(browserName === 'chromium' && video === 'on', 'Same screencast resolution conflicts'); 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(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'); browserTest.fixme(params.id === 'fit' && browserName === 'chromium' && platform === 'darwin', 'High DPI maxes image at 600x600');

View file

@ -473,9 +473,8 @@ it.describe('screencast', () => {
expect(videoFiles.length).toBe(2); 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'); 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'); it.fixme(browserName === 'chromium' && !isHeadlessShell, 'Chromium (but not headless shell) has a min width issue');
const context = await browser.newContext({ const context = await browser.newContext({
@ -723,9 +722,8 @@ it.describe('screencast', () => {
expect(files.length).toBe(1); 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' }); 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 === 'chromium' && !isHeadlessShell, 'The square is not on the video');
it.fixme(browserName === 'firefox' && isWindows, 'https://github.com/microsoft/playwright/issues/14405'); it.fixme(browserName === 'firefox' && isWindows, 'https://github.com/microsoft/playwright/issues/14405');
const size = { width: 600, height: 400 }; const size = { width: 600, height: 400 };
@ -759,9 +757,8 @@ it.describe('screencast', () => {
expectAll(pixels, almostRed); 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' }); 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 === 'chromium' && !isHeadlessShell, 'The square is not on the video');
it.fixme(browserName === 'firefox' && isWindows, 'https://github.com/microsoft/playwright/issues/14405'); 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'); it.fixme(browserName === 'webkit' && isLinux && !headless, 'https://github.com/microsoft/playwright/issues/22617');
@ -797,10 +794,9 @@ it.describe('screencast', () => {
expectAll(pixels, almostRed); 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.skip(trace === 'on');
it.fixme(!headless, 'different trace screencast image size on all browsers'); 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'); it.fixme(browserName === 'chromium' && !isHeadlessShell, 'different trace screencast image size');
const size = { width: 500, height: 400 }; const size = { width: 500, height: 400 };

View file

@ -431,6 +431,9 @@ test('toHaveAccessibleName', async ({ page }) => {
await expect(page.locator('div')).toHaveAccessibleName(/ell\w/); await expect(page.locator('div')).toHaveAccessibleName(/ell\w/);
await expect(page.locator('div')).not.toHaveAccessibleName(/hello/); await expect(page.locator('div')).not.toHaveAccessibleName(/hello/);
await expect(page.locator('div')).toHaveAccessibleName(/hello/, { ignoreCase: true }); await expect(page.locator('div')).toHaveAccessibleName(/hello/, { ignoreCase: true });
await page.setContent(`<button>foo&nbsp;bar\nbaz</button>`);
await expect(page.locator('button')).toHaveAccessibleName('foo bar baz');
}); });
test('toHaveAccessibleDescription', async ({ page }) => { 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')).toHaveAccessibleDescription(/ell\w/);
await expect(page.locator('div')).not.toHaveAccessibleDescription(/hello/); await expect(page.locator('div')).not.toHaveAccessibleDescription(/hello/);
await expect(page.locator('div')).toHaveAccessibleDescription(/hello/, { ignoreCase: true }); await expect(page.locator('div')).toHaveAccessibleDescription(/hello/, { ignoreCase: true });
await page.setContent(`
<div role="button" aria-describedby="desc"></div>
<span id="desc">foo&nbsp;bar\nbaz</span>
`);
await expect(page.locator('div')).toHaveAccessibleDescription('foo bar baz');
}); });
test('toHaveRole', async ({ page }) => { test('toHaveRole', async ({ page }) => {

View file

@ -465,6 +465,12 @@ it('should escape yaml text in text nodes', async ({ page }) => {
<details> <details>
<summary>one: <a href="#">link1</a> "two <a href="#">link2</a> 'three <a href="#">link3</a> \`four</summary> <summary>one: <a href="#">link1</a> "two <a href="#">link2</a> 'three <a href="#">link3</a> \`four</summary>
</details> </details>
<ul>
<a href="#">one</a>,<a href="#">two</a>
(<a href="#">three</a>)
{<a href="#">four</a>}
[<a href="#">five</a>]
</ul>
`); `);
await checkAndMatchSnapshot(page.locator('body'), ` await checkAndMatchSnapshot(page.locator('body'), `
@ -476,6 +482,17 @@ it('should escape yaml text in text nodes', async ({ page }) => {
- text: "'three" - text: "'three"
- link "link3" - link "link3"
- text: "\`four" - text: "\`four"
- list:
- link "one"
- text: ","
- link "two"
- text: (
- link "three"
- text: ") {"
- link "four"
- text: "} ["
- link "five"
- text: "]"
`); `);
}); });
@ -492,3 +509,57 @@ it('should handle long strings', async ({ page }) => {
- region: ${s} - region: ${s}
`); `);
}); });
it('should escape special yaml characters', async ({ page }) => {
await page.setContent(`
<a href="#">@hello</a>@hello
<a href="#">]hello</a>]hello
<a href="#">hello\n</a>
hello\n<a href="#">\n hello</a>\n hello
<a href="#">#hello</a>#hello
`);
await checkAndMatchSnapshot(page.locator('body'), `
- link "@hello"
- text: "@hello"
- link "]hello"
- text: "]hello"
- link "hello"
- text: hello
- link "hello"
- text: hello
- link "#hello"
- text: "#hello"
`);
});
it('should escape special yaml values', async ({ page }) => {
await page.setContent(`
<a href="#">true</a>False
<a href="#">NO</a>yes
<a href="#">y</a>N
<a href="#">on</a>Off
<a href="#">null</a>NULL
<a href="#">123</a>123
<a href="#">-1.2</a>-1.2
<input type=text value="555">
`);
await checkAndMatchSnapshot(page.locator('body'), `
- link "true"
- text: "False"
- link "NO"
- text: "yes"
- link "y"
- text: "N"
- link "on"
- text: "Off"
- link "null"
- text: "NULL"
- link "123"
- text: "123"
- link "-1.2"
- text: "-1.2"
- textbox: "555"
`);
});

View file

@ -307,3 +307,27 @@ test('should show request source context id', async ({ runUITest, server }) => {
await expect(page.getByText('page#2')).toBeVisible(); await expect(page.getByText('page#2')).toBeVisible();
await expect(page.getByText('api#1')).toBeVisible(); await expect(page.getByText('api#1')).toBeVisible();
}); });
test('should filter actions tab on double-click', async ({ runUITest, server }) => {
const { page } = await runUITest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('pass', async ({ page }) => {
await page.goto('${server.EMPTY_PAGE}');
});
`,
});
await page.getByText('pass').dblclick();
const actionsTree = page.getByTestId('actions-tree');
await expect(actionsTree.getByRole('treeitem')).toHaveText([
/Before Hooks/,
/page.goto/,
/After Hooks/,
]);
await actionsTree.getByRole('treeitem', { name: 'page.goto' }).dblclick();
await expect(actionsTree.getByRole('treeitem')).toHaveText([
/page.goto/,
]);
});

View file

@ -24,12 +24,15 @@ function trimPatch(patch: string) {
return patch.split('\n').map(line => line.trimEnd()).join('\n'); 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({ const result = await runInlineTest({
'playwright.config.ts': `
export default { projects: [{ name: 'p1' }, { name: 'p2' }] };
`,
'a.spec.ts': ` 'a.spec.ts': `
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
test('test', async ({ page }) => { 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(\` await expect(page.locator('body')).toMatchAriaSnapshot(\`
- heading "world" - 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 expect(trimPatch(data)).toBe(`diff --git a/a.spec.ts b/a.spec.ts
--- a/a.spec.ts --- a/a.spec.ts
+++ b/a.spec.ts +++ b/a.spec.ts
@@ -3,7 +3,7 @@ @@ -3,7 +3,8 @@
test('test', async ({ page }) => { 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(\` await expect(page.locator('body')).toMatchAriaSnapshot(\`
- - heading "world" - - heading "world"
+ - heading "hello" [level=1] + - heading "hello" [level=1]
+ - heading "bye" [level=2]
\`); \`);
}); });