Compare commits
30 commits
main
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c836cefb12 | ||
|
|
88bc8afc78 | ||
|
|
9e855d5b9a | ||
|
|
9365eb5dac | ||
|
|
f92b2339fe | ||
|
|
008722b2d9 | ||
|
|
1dc8b3cbdc | ||
|
|
fbc770c804 | ||
|
|
1046fe0455 | ||
|
|
1781bf35b3 | ||
|
|
b52a21030f | ||
|
|
2128fac196 | ||
|
|
2ba644852b | ||
|
|
4b0eca4d22 | ||
|
|
e3c5986c5b | ||
|
|
b3aaee0248 | ||
|
|
120cdf664b | ||
|
|
a70a96ab25 | ||
|
|
53f51a8cf1 | ||
|
|
2a00ca8453 | ||
|
|
0e6434013b | ||
|
|
cb0f456e46 | ||
|
|
698823a78e | ||
|
|
c0fa804367 | ||
|
|
7a32228aed | ||
|
|
0e31acea8f | ||
|
|
b2a39ffc61 | ||
|
|
1eea46bd66 | ||
|
|
4c53e56cb4 | ||
|
|
3f36d7ff51 |
31
.github/workflows/tests_secondary.yml
vendored
31
.github/workflows/tests_secondary.yml
vendored
|
|
@ -268,29 +268,8 @@ jobs:
|
||||||
- run: npx playwright install-deps
|
- run: 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
""");
|
""");
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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}",
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
||||||

|
#### 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.
|
||||||
|
|
|
||||||
|
|
@ -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
60
package-lock.json
generated
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,4 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (install)
|
if (install)
|
||||||
install(['chromium', 'ffmpeg']);
|
install(['chromium', 'chromium-headless-shell', 'ffmpeg']);
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,4 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (install)
|
if (install)
|
||||||
install(['chromium', 'ffmpeg']);
|
install(['chromium', 'chromium-headless-shell', 'ffmpeg']);
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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({}));
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ export {
|
||||||
registry,
|
registry,
|
||||||
registryDirectory,
|
registryDirectory,
|
||||||
Registry,
|
Registry,
|
||||||
installDefaultBrowsersForNpmInstall,
|
|
||||||
installBrowsersForNpmInstall,
|
installBrowsersForNpmInstall,
|
||||||
writeDockerVersion } from './registry';
|
writeDockerVersion } from './registry';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -492,9 +492,10 @@ 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 {
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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')) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
27
packages/playwright-core/types/types.d.ts
vendored
27
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
9
packages/playwright/types/test.d.ts
vendored
9
packages/playwright/types/test.d.ts
vendored
|
|
@ -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**
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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 = {};
|
||||||
|
|
|
||||||
|
|
@ -791,7 +791,8 @@ DebugController:
|
||||||
|
|
||||||
highlight:
|
highlight:
|
||||||
parameters:
|
parameters:
|
||||||
selector: string
|
selector: string?
|
||||||
|
ariaTemplate: string?
|
||||||
|
|
||||||
hideHighlight:
|
hideHighlight:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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.`;
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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([]);
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,8 +31,9 @@ type Fixtures = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const test = baseTest.extend<Fixtures>({
|
const test = baseTest.extend<Fixtures>({
|
||||||
wsEndpoint: async ({ }, use) => {
|
wsEndpoint: async ({ headless }, use) => {
|
||||||
process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1';
|
if (headless)
|
||||||
|
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();
|
||||||
await use(wsEndpoint);
|
await use(wsEndpoint);
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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' })`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"'
|
||||||
`);
|
`);
|
||||||
|
|
|
||||||
69
tests/library/inspector/cli-codegen-pick-locator.spec.ts
Normal file
69
tests/library/inspector/cli-codegen-pick-locator.spec.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from './inspectorTest';
|
||||||
|
import { roundBox } from '../../page/pageTest';
|
||||||
|
|
||||||
|
test.describe(() => {
|
||||||
|
test.skip(({ mode }) => mode !== 'default');
|
||||||
|
test.skip(({ trace, codegenMode }) => trace === 'on' && codegenMode === 'trace-events');
|
||||||
|
|
||||||
|
test('should inspect locator', async ({ openRecorder }) => {
|
||||||
|
const { recorder } = await openRecorder();
|
||||||
|
await recorder.setContentAndWait(`<main><button>Submit</button></main>`);
|
||||||
|
await recorder.page.click('x-pw-tool-item.pick-locator');
|
||||||
|
await recorder.page.hover('button');
|
||||||
|
await recorder.trustedClick();
|
||||||
|
await recorder.recorderPage.getByRole('tab', { name: 'Locator' }).click();
|
||||||
|
await expect(recorder.recorderPage.locator('.tab-locator .CodeMirror')).toMatchAriaSnapshot(`
|
||||||
|
- text: "getByRole('button', { name: 'Submit' })"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update locator highlight', async ({ openRecorder }) => {
|
||||||
|
const { recorder } = await openRecorder();
|
||||||
|
await recorder.setContentAndWait(`<main>
|
||||||
|
<button>Submit</button>
|
||||||
|
<button>Cancel</button>
|
||||||
|
</main>`);
|
||||||
|
|
||||||
|
const submitButton = recorder.page.getByRole('button', { name: 'Submit' });
|
||||||
|
const cancelButton = recorder.page.getByRole('button', { name: 'Cancel' });
|
||||||
|
|
||||||
|
await recorder.recorderPage.getByRole('button', { name: 'Record' }).click();
|
||||||
|
|
||||||
|
await recorder.page.click('x-pw-tool-item.pick-locator');
|
||||||
|
await submitButton.hover();
|
||||||
|
await recorder.trustedClick();
|
||||||
|
await recorder.recorderPage.getByRole('tab', { name: 'Locator' }).click();
|
||||||
|
await expect(recorder.recorderPage.locator('.tab-locator .CodeMirror')).toMatchAriaSnapshot(`
|
||||||
|
- text: "getByRole('button', { name: 'Submit' })"
|
||||||
|
`);
|
||||||
|
|
||||||
|
await recorder.recorderPage.locator('.tab-locator .CodeMirror').click();
|
||||||
|
for (let i = 0; i < `Submit' })`.length; i++)
|
||||||
|
await recorder.recorderPage.keyboard.press('Backspace');
|
||||||
|
|
||||||
|
{
|
||||||
|
// Different button.
|
||||||
|
await recorder.recorderPage.locator('.tab-locator .CodeMirror').pressSequentially(`Cancel' })`);
|
||||||
|
await expect(recorder.page.locator('x-pw-highlight')).toBeVisible();
|
||||||
|
const box1 = roundBox(await cancelButton.boundingBox());
|
||||||
|
const box2 = roundBox(await recorder.page.locator('x-pw-highlight').boundingBox());
|
||||||
|
expect(box1).toEqual(box2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -145,7 +145,7 @@ it.describe('permissions', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support clipboard read', async ({ page, context, server, browserName, isWindows, isLinux, headless, channel }) => {
|
it('should support clipboard read', async ({ page, context, server, browserName, isWindows, isLinux, headless, isHeadlessShell }) => {
|
||||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/27475' });
|
it.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');
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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.`);
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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 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 bar\nbaz</span>
|
||||||
|
`);
|
||||||
|
await expect(page.locator('div')).toHaveAccessibleDescription('foo bar baz');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('toHaveRole', async ({ page }) => {
|
test('toHaveRole', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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/,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
\`);
|
\`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue