Compare commits
24 commits
main
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ede66abd6 | ||
|
|
76dc43e9ba | ||
|
|
f38b2c86eb | ||
|
|
0d5098b7c0 | ||
|
|
bbe27dc564 | ||
|
|
226eedf019 | ||
|
|
36f8a6399c | ||
|
|
d284219c32 | ||
|
|
8f9bf0f0ac | ||
|
|
c3335e4619 | ||
|
|
5459be0585 | ||
|
|
553a211b65 | ||
|
|
f7087bfe3b | ||
|
|
2720c72f65 | ||
|
|
74314712fb | ||
|
|
ba1a1bd99d | ||
|
|
09c2d891ef | ||
|
|
e20be6c01e | ||
|
|
5ceac3fe5d | ||
|
|
c6d7cb4fd8 | ||
|
|
2fd36db85e | ||
|
|
db93e9766e | ||
|
|
0cf4d9455d | ||
|
|
2e484b46a2 |
|
|
@ -1,6 +1,6 @@
|
|||
# 🎭 Playwright
|
||||
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop -->
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop -->
|
||||
|
||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->108.0.5359.29<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->16.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->16.4<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->106.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/library#system-requirements) for details.
|
||||
|
|
|
|||
|
|
@ -949,6 +949,7 @@ Attribute name to get the value for.
|
|||
### param: Frame.getByRole.role = %%-locator-get-by-role-role-%%
|
||||
### option: Frame.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
|
||||
* since: v1.27
|
||||
### option: Frame.getByRole.exact = %%-locator-get-by-role-option-exact-%%
|
||||
|
||||
|
||||
## method: Frame.getByTestId
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ in that iframe.
|
|||
### param: FrameLocator.getByRole.role = %%-locator-get-by-role-role-%%
|
||||
### option: FrameLocator.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
|
||||
* since: v1.27
|
||||
### option: FrameLocator.getByRole.exact = %%-locator-get-by-role-option-exact-%%
|
||||
|
||||
|
||||
## method: FrameLocator.getByTestId
|
||||
|
|
|
|||
|
|
@ -695,6 +695,7 @@ Attribute name to get the value for.
|
|||
### param: Locator.getByRole.role = %%-locator-get-by-role-role-%%
|
||||
### option: Locator.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
|
||||
* since: v1.27
|
||||
### option: Locator.getByRole.exact = %%-locator-get-by-role-option-exact-%%
|
||||
|
||||
|
||||
## method: Locator.getByTestId
|
||||
|
|
|
|||
|
|
@ -2222,6 +2222,7 @@ Attribute name to get the value for.
|
|||
### param: Page.getByRole.role = %%-locator-get-by-role-role-%%
|
||||
### option: Page.getByRole.-inline- = %%-locator-get-by-role-option-list-v1.27-%%
|
||||
* since: v1.27
|
||||
### option: Page.getByRole.exact = %%-locator-get-by-role-option-exact-%%
|
||||
|
||||
|
||||
## method: Page.getByTestId
|
||||
|
|
|
|||
|
|
@ -112,10 +112,18 @@ If set changes the request method (e.g. GET or POST)
|
|||
|
||||
### option: Route.continue.postData
|
||||
* since: v1.8
|
||||
* langs: js, python, java
|
||||
- `postData` <[string]|[Buffer]>
|
||||
|
||||
If set changes the post data of request
|
||||
|
||||
### option: Route.continue.postData
|
||||
* since: v1.8
|
||||
* langs: csharp
|
||||
- `postData` <[Buffer]>
|
||||
|
||||
If set changes the post data of request
|
||||
|
||||
### option: Route.continue.headers
|
||||
* since: v1.8
|
||||
- `headers` <[Object]<[string], [string]>>
|
||||
|
|
@ -378,10 +386,18 @@ If set changes the request method (e.g. GET or POST)
|
|||
|
||||
### option: Route.fallback.postData
|
||||
* since: v1.23
|
||||
* langs: js, python, java
|
||||
- `postData` <[string]|[Buffer]>
|
||||
|
||||
If set changes the post data of request
|
||||
|
||||
### option: Route.fallback.postData
|
||||
* since: v1.23
|
||||
* langs: csharp
|
||||
- `postData` <[Buffer]>
|
||||
|
||||
If set changes the post data of request
|
||||
|
||||
### option: Route.fallback.headers
|
||||
* since: v1.23
|
||||
- `headers` <[Object]<[string], [string]>>
|
||||
|
|
|
|||
|
|
@ -1109,7 +1109,7 @@ Required aria role.
|
|||
* since: v1.27
|
||||
- `checked` <[boolean]>
|
||||
|
||||
An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for checked are `true`, `false` and `"mixed"`.
|
||||
An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
|
||||
|
||||
Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked).
|
||||
|
||||
|
|
@ -1117,7 +1117,7 @@ Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-check
|
|||
* since: v1.27
|
||||
- `disabled` <[boolean]>
|
||||
|
||||
A boolean attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
An attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
|
||||
:::note
|
||||
Unlike most other attributes, `disabled` is inherited through the DOM hierarchy.
|
||||
|
|
@ -1128,7 +1128,7 @@ Learn more about [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disa
|
|||
* since: v1.27
|
||||
- `expanded` <[boolean]>
|
||||
|
||||
A boolean attribute that is usually set by `aria-expanded`.
|
||||
An attribute that is usually set by `aria-expanded`.
|
||||
|
||||
Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded).
|
||||
|
||||
|
|
@ -1136,7 +1136,7 @@ A boolean attribute that is usually set by `aria-expanded`.
|
|||
* since: v1.27
|
||||
- `includeHidden` <[boolean]>
|
||||
|
||||
A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector.
|
||||
Option that controls whether hidden elements are matched. By default, only non-hidden elements, as [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector.
|
||||
|
||||
Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden).
|
||||
|
||||
|
|
@ -1152,15 +1152,21 @@ Learn more about [`aria-level`](https://www.w3.org/TR/wai-aria-1.2/#aria-level).
|
|||
* since: v1.27
|
||||
- `name` <[string]|[RegExp]>
|
||||
|
||||
A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
Option to match the [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). By default, matching is case-insensitive and searches for a substring, use [`option: exact`] to control this behavior.
|
||||
|
||||
Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
|
||||
## locator-get-by-role-option-exact
|
||||
* since: v1.28
|
||||
- `exact` <[boolean]>
|
||||
|
||||
Whether [`option: name`] is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when [`option: name`] is a regular expression. Note that exact match still trims whitespace.
|
||||
|
||||
## locator-get-by-role-option-pressed
|
||||
* since: v1.27
|
||||
- `pressed` <[boolean]>
|
||||
|
||||
An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`.
|
||||
An attribute that is usually set by `aria-pressed`.
|
||||
|
||||
Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed).
|
||||
|
||||
|
|
@ -1168,7 +1174,7 @@ Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-press
|
|||
* since: v1.27
|
||||
- `selected` <boolean>
|
||||
|
||||
A boolean attribute that is usually set by `aria-selected`.
|
||||
An attribute that is usually set by `aria-selected`.
|
||||
|
||||
Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected).
|
||||
|
||||
|
|
|
|||
|
|
@ -99,17 +99,20 @@ with sync_playwright() as playwright:
|
|||
|
||||
To have the extension loaded when running tests you can use a test fixture to set the context. You can also dynamically retrieve the extension id and use it to load and test the popup page for example.
|
||||
|
||||
First, add fixtures that will load the extension:
|
||||
|
||||
```ts
|
||||
import { test as base, expect, BrowserContext } from "@playwright/test";
|
||||
import path from "path";
|
||||
// fixtures.ts
|
||||
import { test as base, expect, chromium, type BrowserContext } from '@playwright/test';
|
||||
import path from 'path';
|
||||
|
||||
export const test = base.extend<{
|
||||
context: BrowserContext;
|
||||
extensionId: string;
|
||||
}>({
|
||||
context: async ({ }, use) => {
|
||||
const pathToExtension = path.join(__dirname, "my-extension");
|
||||
const context = await chromium.launchPersistentContext("", {
|
||||
const pathToExtension = path.join(__dirname, 'my-extension');
|
||||
const context = await chromium.launchPersistentContext('', {
|
||||
headless: false,
|
||||
args: [
|
||||
`--disable-extensions-except=${pathToExtension}`,
|
||||
|
|
@ -124,31 +127,22 @@ export const test = base.extend<{
|
|||
// for manifest v2:
|
||||
let [background] = context.backgroundPages()
|
||||
if (!background)
|
||||
background = await context.waitForEvent("backgroundpage")
|
||||
background = await context.waitForEvent('backgroundpage')
|
||||
*/
|
||||
|
||||
// for manifest v3:
|
||||
let [background] = context.serviceWorkers();
|
||||
if (!background)
|
||||
background = await context.waitForEvent("serviceworker");
|
||||
background = await context.waitForEvent('serviceworker');
|
||||
|
||||
const extensionId = background.url().split("/")[2];
|
||||
const extensionId = background.url().split('/')[2];
|
||||
await use(extensionId);
|
||||
},
|
||||
});
|
||||
|
||||
test("example test", async ({ page }) => {
|
||||
await page.goto("https://example.com");
|
||||
await expect(page.locator("body")).toHaveText("Changed by my-extension");
|
||||
});
|
||||
|
||||
test("popup page", async ({ page, extensionId }) => {
|
||||
await page.goto(`chrome-extension://${extensionId}/popup.html`);
|
||||
await expect(page.locator("body")).toHaveText("my-extension popup");
|
||||
});
|
||||
export const expect = test.expect;
|
||||
```
|
||||
|
||||
```py
|
||||
```python
|
||||
# conftest.py
|
||||
from typing import Generator
|
||||
from pathlib import Path
|
||||
|
|
@ -188,7 +182,23 @@ def extension_id(context) -> Generator[str, None, None]:
|
|||
|
||||
```
|
||||
|
||||
```py
|
||||
Then use these fixtures in a test:
|
||||
|
||||
```ts
|
||||
import { test, expect } from './fixtures';
|
||||
|
||||
test('example test', async ({ page }) => {
|
||||
await page.goto('https://example.com');
|
||||
await expect(page.locator('body')).toHaveText('Changed by my-extension');
|
||||
});
|
||||
|
||||
test('popup page', async ({ page, extensionId }) => {
|
||||
await page.goto(`chrome-extension://${extensionId}/popup.html`);
|
||||
await expect(page.locator('body')).toHaveText('my-extension popup');
|
||||
});
|
||||
```
|
||||
|
||||
```python
|
||||
# test_foo.py
|
||||
from playwright.sync_api import expect, Page
|
||||
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ steps:
|
|||
name: 'Playwright Tests'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.28.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.28.1-focal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v2
|
||||
|
|
@ -194,7 +194,7 @@ steps:
|
|||
name: 'Playwright Tests'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.28.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.28.1-focal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
|
|
@ -218,7 +218,7 @@ steps:
|
|||
name: 'Playwright Tests'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.28.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.28.1-focal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v3
|
||||
|
|
@ -239,7 +239,7 @@ steps:
|
|||
name: 'Playwright Tests'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.28.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.28.1-focal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup dotnet
|
||||
|
|
@ -264,7 +264,7 @@ steps:
|
|||
name: 'Playwright Tests - ${{ matrix.project }} - Shard ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }}'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.28.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.28.1-focal
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
@ -299,7 +299,7 @@ jobs:
|
|||
- deployment: Run_E2E_Tests
|
||||
pool:
|
||||
vmImage: ubuntu-20.04
|
||||
container: mcr.microsoft.com/playwright:v1.28.0-focal
|
||||
container: mcr.microsoft.com/playwright:v1.28.1-focal
|
||||
environment: testing
|
||||
strategy:
|
||||
runOnce:
|
||||
|
|
@ -325,7 +325,7 @@ jobs:
|
|||
- deployment: Run_E2E_Tests
|
||||
pool:
|
||||
vmImage: ubuntu-20.04
|
||||
container: mcr.microsoft.com/playwright:v1.28.0-focal
|
||||
container: mcr.microsoft.com/playwright:v1.28.1-focal
|
||||
environment: testing
|
||||
strategy:
|
||||
runOnce:
|
||||
|
|
@ -368,7 +368,7 @@ Running Playwright on Circle CI is very similar to running on GitHub Actions. In
|
|||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.28.0-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.28.1-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed if playwright is in `devDependencies`
|
||||
```
|
||||
|
|
@ -404,7 +404,7 @@ to run tests on Jenkins.
|
|||
|
||||
```groovy
|
||||
pipeline {
|
||||
agent { docker { image 'mcr.microsoft.com/playwright:v1.28.0-focal' } }
|
||||
agent { docker { image 'mcr.microsoft.com/playwright:v1.28.1-focal' } }
|
||||
stages {
|
||||
stage('e2e-tests') {
|
||||
steps {
|
||||
|
|
@ -422,7 +422,7 @@ pipeline {
|
|||
Bitbucket Pipelines can use public [Docker images as build environments](https://confluence.atlassian.com/bitbucket/use-docker-images-as-build-environments-792298897.html). To run Playwright tests on Bitbucket, use our public Docker image ([see Dockerfile](./docker.md)).
|
||||
|
||||
```yml
|
||||
image: mcr.microsoft.com/playwright:v1.28.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.28.1-focal
|
||||
```
|
||||
|
||||
### GitLab CI
|
||||
|
|
@ -435,7 +435,7 @@ stages:
|
|||
|
||||
tests:
|
||||
stage: test
|
||||
image: mcr.microsoft.com/playwright:v1.28.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.28.1-focal
|
||||
script:
|
||||
...
|
||||
```
|
||||
|
|
@ -451,7 +451,7 @@ stages:
|
|||
|
||||
tests:
|
||||
stage: test
|
||||
image: mcr.microsoft.com/playwright:v1.28.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.28.1-focal
|
||||
parallel: 7
|
||||
script:
|
||||
- npm ci
|
||||
|
|
@ -466,7 +466,7 @@ stages:
|
|||
|
||||
tests:
|
||||
stage: test
|
||||
image: mcr.microsoft.com/playwright:v1.28.0-focal
|
||||
image: mcr.microsoft.com/playwright:v1.28.1-focal
|
||||
parallel:
|
||||
matrix:
|
||||
- PROJECT: ['chromium', 'webkit']
|
||||
|
|
|
|||
|
|
@ -14,19 +14,19 @@ This image is published on [Docker Hub].
|
|||
### Pull the image
|
||||
|
||||
```bash js
|
||||
docker pull mcr.microsoft.com/playwright:v1.28.0-focal
|
||||
docker pull mcr.microsoft.com/playwright:v1.28.1-focal
|
||||
```
|
||||
|
||||
```bash python
|
||||
docker pull mcr.microsoft.com/playwright/python:v1.28.0-focal
|
||||
docker pull mcr.microsoft.com/playwright/python:v1.28.1-focal
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
docker pull mcr.microsoft.com/playwright/dotnet:v1.28.0-focal
|
||||
docker pull mcr.microsoft.com/playwright/dotnet:v1.28.1-focal
|
||||
```
|
||||
|
||||
```bash java
|
||||
docker pull mcr.microsoft.com/playwright/java:v1.28.0-focal
|
||||
docker pull mcr.microsoft.com/playwright/java:v1.28.1-focal
|
||||
```
|
||||
|
||||
### Run the image
|
||||
|
|
@ -38,19 +38,19 @@ By default, the Docker image will use the `root` user to run the browsers. This
|
|||
On trusted websites, you can avoid creating a separate user and use root for it since you trust the code which will run on the browsers.
|
||||
|
||||
```bash js
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright:v1.28.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright:v1.28.1-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash python
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/python:v1.28.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/python:v1.28.1-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/dotnet:v1.28.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/dotnet:v1.28.1-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash java
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/java:v1.28.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host mcr.microsoft.com/playwright/java:v1.28.1-focal /bin/bash
|
||||
```
|
||||
|
||||
#### Crawling and scraping
|
||||
|
|
@ -58,19 +58,19 @@ docker run -it --rm --ipc=host mcr.microsoft.com/playwright/java:v1.28.0-focal /
|
|||
On untrusted websites, it's recommended to use a separate user for launching the browsers in combination with the seccomp profile. Inside the container or if you are using the Docker image as a base image you have to use `adduser` for it.
|
||||
|
||||
```bash js
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright:v1.28.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright:v1.28.1-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash python
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/python:v1.28.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/python:v1.28.1-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/dotnet:v1.28.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/dotnet:v1.28.1-focal /bin/bash
|
||||
```
|
||||
|
||||
```bash java
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/java:v1.28.0-focal /bin/bash
|
||||
docker run -it --rm --ipc=host --user pwuser --security-opt seccomp=seccomp_profile.json mcr.microsoft.com/playwright/java:v1.28.1-focal /bin/bash
|
||||
```
|
||||
|
||||
[`seccomp_profile.json`](https://github.com/microsoft/playwright/blob/main/utils/docker/seccomp_profile.json) is needed to run Chromium with sandbox. This is a [default Docker seccomp profile](https://github.com/docker/engine/blob/d0d99b04cf6e00ed3fc27e81fc3d94e7eda70af3/profiles/seccomp/default.json) with extra user namespace cloning permissions:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -4,6 +4,30 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.28
|
||||
|
||||
### Playwright Tools
|
||||
|
||||
* **Live Locators in CodeGen.** Generate a locator for any element on the page using "Explore" tool.
|
||||
|
||||

|
||||
|
||||
### New APIs
|
||||
|
||||
- [`method: Locator.blur`]
|
||||
- [`method: Locator.clear`]
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 108.0.5359.29
|
||||
* Mozilla Firefox 106.0
|
||||
* WebKit 16.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 107
|
||||
* Microsoft Edge 107
|
||||
|
||||
## Version 1.27
|
||||
|
||||
### Locators
|
||||
|
|
|
|||
|
|
@ -4,6 +4,31 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.28
|
||||
|
||||
### Playwright Tools
|
||||
|
||||
* **Live Locators in CodeGen.** Generate a locator for any element on the page using "Explore" tool.
|
||||
|
||||

|
||||
|
||||
### New APIs
|
||||
|
||||
- [`method: Locator.blur`]
|
||||
- [`method: Locator.clear`]
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 108.0.5359.29
|
||||
* Mozilla Firefox 106.0
|
||||
* WebKit 16.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 107
|
||||
* Microsoft Edge 107
|
||||
|
||||
|
||||
## Version 1.27
|
||||
|
||||
### Locators
|
||||
|
|
|
|||
|
|
@ -4,6 +4,68 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.28
|
||||
|
||||
### Playwright Tools
|
||||
|
||||
* **Record at Cursor in VSCode.** You can run the test, position the cursor at the end of the test and continue generating the test.
|
||||
|
||||

|
||||
|
||||
* **Live Locators in VSCode.** You can hover and edit locators in VSCode to get them highlighted in the opened browser.
|
||||
* **Live Locators in CodeGen.** Generate a locator for any element on the page using "Explore" tool.
|
||||
|
||||

|
||||
|
||||
* **Codegen and Trace Viewer Dark Theme.** Automatically picked up from operating system settings.
|
||||
|
||||

|
||||
|
||||
|
||||
### Test Runner
|
||||
|
||||
* Configure retries and test timeout for a file or a test with [`method: Test.describe.configure`].
|
||||
|
||||
```js
|
||||
// Each test in the file will be retried twice and have a timeout of 20 seconds.
|
||||
test.describe.configure({ retries: 2, timeout: 20_000 });
|
||||
test('runs first', async ({ page }) => {});
|
||||
test('runs second', async ({ page }) => {});
|
||||
```
|
||||
|
||||
* Use [`property: TestProject.snapshotPathTemplate`] and [`property: TestConfig.snapshotPathTemplate`] to configure a template controlling location of snapshots generated by [`method: PageAssertions.toHaveScreenshot#1`] and [`method: ScreenshotAssertions.toMatchSnapshot#1`].
|
||||
|
||||
```js
|
||||
// playwright.config.ts
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: './tests',
|
||||
snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### New APIs
|
||||
|
||||
- [`method: Locator.blur`]
|
||||
- [`method: Locator.clear`]
|
||||
- [`method: Android.launchServer`] and [`method: Android.connect`]
|
||||
- [`event: AndroidDevice.close`]
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 108.0.5359.29
|
||||
* Mozilla Firefox 106.0
|
||||
* WebKit 16.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 107
|
||||
* Microsoft Edge 107
|
||||
|
||||
|
||||
## Version 1.27
|
||||
|
||||
<div className="embed-youtube">
|
||||
|
|
@ -152,7 +214,7 @@ This version was also tested against the following stable channels:
|
|||
|
||||
### Announcements
|
||||
|
||||
* 🎁 We now ship Ubuntu 22.04 Jammy Jellyfish docker image: `mcr.microsoft.com/playwright:v1.28.0-jammy`.
|
||||
* 🎁 We now ship Ubuntu 22.04 Jammy Jellyfish docker image: `mcr.microsoft.com/playwright:v1.28.1-jammy`.
|
||||
* 🪦 This is the last release with macOS 10.15 support (deprecated as of 1.21).
|
||||
* 🪦 This is the last release with Node.js 12 support, we recommend upgrading to Node.js LTS (16).
|
||||
* ⚠️ Ubuntu 18 is now deprecated and will not be supported as of Dec 2022.
|
||||
|
|
@ -402,7 +464,7 @@ Read more about [component testing with Playwright](./test-components).
|
|||
}
|
||||
});
|
||||
```
|
||||
* Playwright now runs on Ubuntu 22 amd64 and Ubuntu 22 arm64. We also publish new docker image `mcr.microsoft.com/playwright:v1.28.0-jammy`.
|
||||
* Playwright now runs on Ubuntu 22 amd64 and Ubuntu 22 arm64. We also publish new docker image `mcr.microsoft.com/playwright:v1.28.1-jammy`.
|
||||
|
||||
### ⚠️ Breaking Changes ⚠️
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,31 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.28
|
||||
|
||||
### Playwright Tools
|
||||
|
||||
* **Live Locators in CodeGen.** Generate a locator for any element on the page using "Explore" tool.
|
||||
|
||||

|
||||
|
||||
### New APIs
|
||||
|
||||
- [`method: Locator.blur`]
|
||||
- [`method: Locator.clear`]
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 108.0.5359.29
|
||||
* Mozilla Firefox 106.0
|
||||
* WebKit 16.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 107
|
||||
* Microsoft Edge 107
|
||||
|
||||
|
||||
## Version 1.27
|
||||
|
||||
### Locators
|
||||
|
|
@ -97,7 +122,7 @@ This version was also tested against the following stable channels:
|
|||
|
||||
### Announcements
|
||||
|
||||
* 🎁 We now ship Ubuntu 22.04 Jammy Jellyfish docker image: `mcr.microsoft.com/playwright/python:v1.28.0-jammy`.
|
||||
* 🎁 We now ship Ubuntu 22.04 Jammy Jellyfish docker image: `mcr.microsoft.com/playwright/python:v1.28.1-jammy`.
|
||||
* 🪦 This is the last release with macOS 10.15 support (deprecated as of 1.21).
|
||||
* ⚠️ Ubuntu 18 is now deprecated and will not be supported as of Dec 2022.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
# class: Storage
|
||||
* since: v1.28
|
||||
* langs: js
|
||||
|
||||
Playwright Test provides a [`method: TestInfo.storage`] object for passing values between project setup and tests.
|
||||
TODO: examples
|
||||
|
||||
## async method: Storage.get
|
||||
* since: v1.28
|
||||
- returns: <[any]>
|
||||
|
||||
Get named item from the storage. Returns undefined if there is no value with given name.
|
||||
|
||||
### param: Storage.get.name
|
||||
* since: v1.28
|
||||
- `name` <[string]>
|
||||
|
||||
Item name.
|
||||
|
||||
## async method: Storage.set
|
||||
* since: v1.28
|
||||
|
||||
Set value to the storage.
|
||||
|
||||
### param: Storage.set.name
|
||||
* since: v1.28
|
||||
- `name` <[string]>
|
||||
|
||||
Item name.
|
||||
|
||||
### param: Storage.set.value
|
||||
* since: v1.28
|
||||
- `value` <[any]>
|
||||
|
||||
Item value. The value must be serializable to JSON. Passing `undefined` deletes the entry with given name.
|
||||
|
||||
|
|
@ -505,12 +505,6 @@ Output written to `process.stderr` or `console.error` during the test execution.
|
|||
|
||||
Output written to `process.stdout` or `console.log` during the test execution.
|
||||
|
||||
## method: TestInfo.storage
|
||||
* since: v1.28
|
||||
- returns: <[Storage]>
|
||||
|
||||
Returns a [Storage] instance for the currently running project.
|
||||
|
||||
## property: TestInfo.timeout
|
||||
* since: v1.10
|
||||
- type: <[int]>
|
||||
|
|
|
|||
|
|
@ -202,14 +202,6 @@ Learn more about [automatic screenshots](../test-configuration.md#automatic-scre
|
|||
## property: TestOptions.storageState = %%-js-python-context-option-storage-state-%%
|
||||
* since: v1.10
|
||||
|
||||
## property: TestOptions.storageStateName
|
||||
* since: v1.28
|
||||
- type: <[string]>
|
||||
|
||||
Name of the [Storage] entry that should be used to initialize [`property: TestOptions.storageState`]. The value must be
|
||||
written to the storage before creatiion of a browser context that uses it (usually in [`property: TestProject.setup`]). If both
|
||||
this property and [`property: TestOptions.storageState`] are specified, this property will always take precedence.
|
||||
|
||||
## property: TestOptions.testIdAttribute
|
||||
* since: v1.27
|
||||
|
||||
|
|
|
|||
|
|
@ -162,12 +162,6 @@ Metadata that will be put directly to the test report serialized as JSON.
|
|||
|
||||
Project name is visible in the report and during test execution.
|
||||
|
||||
## property: TestProject.setup
|
||||
* since: v1.28
|
||||
- type: ?<[string]|[RegExp]|[Array]<[string]|[RegExp]>>
|
||||
|
||||
Project setup files that would be executed before all tests in the project. If project setup fails the tests in this project will be skipped. All project setup files will run in every shard if the project is sharded.
|
||||
|
||||
## property: TestProject.snapshotDir
|
||||
* since: v1.10
|
||||
- type: ?<[string]>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ The snapshot name `example-test-1-chromium-darwin.png` consists of a few parts:
|
|||
If you are not on the same operating system as your CI system, you can use Docker to generate/update the screenshots:
|
||||
|
||||
```bash
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.28.0-focal /bin/bash
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.28.1-focal /bin/bash
|
||||
npm install
|
||||
npx playwright test --update-snapshots
|
||||
```
|
||||
|
|
|
|||
66
package-lock.json
generated
66
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "playwright-internal",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
|
@ -5858,11 +5858,11 @@
|
|||
"version": "0.0.0"
|
||||
},
|
||||
"packages/playwright": {
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -5872,11 +5872,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-chromium": {
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -5886,7 +5886,7 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-core": {
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -5897,10 +5897,10 @@
|
|||
},
|
||||
"packages/playwright-ct-react": {
|
||||
"name": "@playwright/experimental-ct-react",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.28.0-next",
|
||||
"@playwright/test": "1.28.1",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"vite": "^3.2.1"
|
||||
},
|
||||
|
|
@ -5937,10 +5937,10 @@
|
|||
},
|
||||
"packages/playwright-ct-solid": {
|
||||
"name": "@playwright/experimental-ct-solid",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.28.0-next",
|
||||
"@playwright/test": "1.28.1",
|
||||
"vite": "^3.2.1",
|
||||
"vite-plugin-solid": "^2.3.10"
|
||||
},
|
||||
|
|
@ -5953,10 +5953,10 @@
|
|||
},
|
||||
"packages/playwright-ct-svelte": {
|
||||
"name": "@playwright/experimental-ct-svelte",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.28.0-next",
|
||||
"@playwright/test": "1.28.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.1.0",
|
||||
"vite": "^3.2.1"
|
||||
},
|
||||
|
|
@ -5969,10 +5969,10 @@
|
|||
},
|
||||
"packages/playwright-ct-vue": {
|
||||
"name": "@playwright/experimental-ct-vue",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.28.0-next",
|
||||
"@playwright/test": "1.28.1",
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"vite": "^3.2.1"
|
||||
},
|
||||
|
|
@ -6018,10 +6018,10 @@
|
|||
},
|
||||
"packages/playwright-ct-vue2": {
|
||||
"name": "@playwright/experimental-ct-vue2",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.28.0-next",
|
||||
"@playwright/test": "1.28.1",
|
||||
"@vitejs/plugin-vue2": "^2.0.0",
|
||||
"vite": "^3.2.1"
|
||||
},
|
||||
|
|
@ -6033,11 +6033,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-firefox": {
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -6048,11 +6048,11 @@
|
|||
},
|
||||
"packages/playwright-test": {
|
||||
"name": "@playwright/test",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -6062,11 +6062,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-webkit": {
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -6637,7 +6637,7 @@
|
|||
"@playwright/experimental-ct-react": {
|
||||
"version": "file:packages/playwright-ct-react",
|
||||
"requires": {
|
||||
"@playwright/test": "1.28.0-next",
|
||||
"@playwright/test": "1.28.1",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"vite": "^3.2.1"
|
||||
},
|
||||
|
|
@ -6664,7 +6664,7 @@
|
|||
"@playwright/experimental-ct-solid": {
|
||||
"version": "file:packages/playwright-ct-solid",
|
||||
"requires": {
|
||||
"@playwright/test": "1.28.0-next",
|
||||
"@playwright/test": "1.28.1",
|
||||
"solid-js": "^1.4.7",
|
||||
"vite": "^3.2.1",
|
||||
"vite-plugin-solid": "^2.3.10"
|
||||
|
|
@ -6673,7 +6673,7 @@
|
|||
"@playwright/experimental-ct-svelte": {
|
||||
"version": "file:packages/playwright-ct-svelte",
|
||||
"requires": {
|
||||
"@playwright/test": "1.28.0-next",
|
||||
"@playwright/test": "1.28.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.1.0",
|
||||
"svelte": "^3.49.0",
|
||||
"vite": "^3.2.1"
|
||||
|
|
@ -6682,7 +6682,7 @@
|
|||
"@playwright/experimental-ct-vue": {
|
||||
"version": "file:packages/playwright-ct-vue",
|
||||
"requires": {
|
||||
"@playwright/test": "1.28.0-next",
|
||||
"@playwright/test": "1.28.1",
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"vite": "^3.2.1"
|
||||
},
|
||||
|
|
@ -6717,7 +6717,7 @@
|
|||
"@playwright/experimental-ct-vue2": {
|
||||
"version": "file:packages/playwright-ct-vue2",
|
||||
"requires": {
|
||||
"@playwright/test": "1.28.0-next",
|
||||
"@playwright/test": "1.28.1",
|
||||
"@vitejs/plugin-vue2": "^2.0.0",
|
||||
"vite": "^3.2.1",
|
||||
"vue": "^2.7.13"
|
||||
|
|
@ -6727,7 +6727,7 @@
|
|||
"version": "file:packages/playwright-test",
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
}
|
||||
},
|
||||
"@rollup/pluginutils": {
|
||||
|
|
@ -8831,13 +8831,13 @@
|
|||
"playwright": {
|
||||
"version": "file:packages/playwright",
|
||||
"requires": {
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
}
|
||||
},
|
||||
"playwright-chromium": {
|
||||
"version": "file:packages/playwright-chromium",
|
||||
"requires": {
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
|
|
@ -8846,13 +8846,13 @@
|
|||
"playwright-firefox": {
|
||||
"version": "file:packages/playwright-firefox",
|
||||
"requires": {
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
}
|
||||
},
|
||||
"playwright-webkit": {
|
||||
"version": "file:packages/playwright-webkit",
|
||||
"requires": {
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"private": true,
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-chromium",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"description": "A high-level API to automate Chromium",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,6 +28,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"mac10.15": "1616",
|
||||
"ubuntu18.04": "1728"
|
||||
},
|
||||
"browserVersion": "16.0"
|
||||
"browserVersion": "16.4"
|
||||
},
|
||||
{
|
||||
"name": "ffmpeg",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-core",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
|
|||
|
|
@ -208,6 +208,8 @@ export class PlaywrightConnection {
|
|||
for (const context of browser.contexts()) {
|
||||
if (!context.pages().length)
|
||||
await context.close(serverSideCallMetadata());
|
||||
else
|
||||
await context.stopPendingOperations();
|
||||
}
|
||||
if (!browser.contexts())
|
||||
await browser.close();
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import { gracefullyCloseSet } from '../../utils/processLauncher';
|
|||
import { TimeoutSettings } from '../../common/timeoutSettings';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import { SdkObject, serverSideCallMetadata } from '../instrumentation';
|
||||
import { DEFAULT_ARGS } from '../chromium/chromium';
|
||||
import { chromiumSwitches } from '../chromium/chromiumSwitches';
|
||||
import { registry } from '../registry';
|
||||
|
||||
const ARTIFACTS_FOLDER = path.join(os.tmpdir(), 'playwright-artifacts-');
|
||||
|
|
@ -269,7 +269,7 @@ export class AndroidDevice extends SdkObject {
|
|||
'--disable-fre',
|
||||
'--no-default-browser-check',
|
||||
`--remote-debugging-socket-name=${socketName}`,
|
||||
...DEFAULT_ARGS,
|
||||
...chromiumSwitches,
|
||||
].join(' ');
|
||||
debug('pw:android')('Starting', pkg, commandLine);
|
||||
await this._backend.runCommand(`shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`);
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ export abstract class Browser extends SdkObject {
|
|||
this._contextForReuse = { context: await this.newContext(metadata, params), hash };
|
||||
return { context: this._contextForReuse.context, needsReset: false };
|
||||
}
|
||||
await this._contextForReuse.context.stopPendingOperations();
|
||||
return { context: this._contextForReuse.context, needsReset: true };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import { helper } from './helper';
|
|||
import * as network from './network';
|
||||
import type { PageDelegate } from './page';
|
||||
import { Page, PageBinding } from './page';
|
||||
import type { Progress } from './progress';
|
||||
import type { Progress, ProgressController } from './progress';
|
||||
import type { Selectors } from './selectors';
|
||||
import type * as types from './types';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
|
@ -56,6 +56,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
|
||||
readonly _timeoutSettings = new TimeoutSettings();
|
||||
readonly _pageBindings = new Map<string, PageBinding>();
|
||||
readonly _activeProgressControllers = new Set<ProgressController>();
|
||||
readonly _options: channels.BrowserNewContextParams;
|
||||
_requestInterceptor?: network.RouteHandler;
|
||||
private _isPersistentContext: boolean;
|
||||
|
|
@ -145,6 +146,11 @@ export abstract class BrowserContext extends SdkObject {
|
|||
return true;
|
||||
}
|
||||
|
||||
async stopPendingOperations() {
|
||||
for (const controller of this._activeProgressControllers)
|
||||
controller.abort(new Error(`Context was reset for reuse.`));
|
||||
}
|
||||
|
||||
static reusableContextHash(params: channels.BrowserNewContextForReuseParams): string {
|
||||
const paramsCopy = { ...params };
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import https from 'https';
|
|||
import { registry } from '../registry';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { validateBrowserContextOptions } from '../browserContext';
|
||||
import { chromiumSwitches } from './chromiumSwitches';
|
||||
|
||||
const ARTIFACTS_FOLDER = path.join(os.tmpdir(), 'playwright-artifacts-');
|
||||
|
||||
|
|
@ -282,7 +283,7 @@ export class Chromium extends BrowserType {
|
|||
throw new Error('Playwright manages remote debugging connection itself.');
|
||||
if (args.find(arg => !arg.startsWith('-')))
|
||||
throw new Error('Arguments can not specify page to be opened');
|
||||
const chromeArguments = [...DEFAULT_ARGS];
|
||||
const chromeArguments = [...chromiumSwitches];
|
||||
|
||||
// See https://github.com/microsoft/playwright/issues/7362
|
||||
if (os.platform() === 'darwin')
|
||||
|
|
@ -325,42 +326,6 @@ export class Chromium extends BrowserType {
|
|||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_ARGS = [
|
||||
'--disable-field-trial-config', // https://source.chromium.org/chromium/chromium/src/+/main:testing/variations/README.md
|
||||
'--disable-background-networking',
|
||||
'--enable-features=NetworkService,NetworkServiceInProcess',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
'--disable-back-forward-cache', // Avoids surprises like main request not being intercepted during page.goBack().
|
||||
'--disable-breakpad',
|
||||
'--disable-client-side-phishing-detection',
|
||||
'--disable-component-extensions-with-background-pages',
|
||||
'--disable-component-update', // Avoids unneeded network activity after startup.
|
||||
'--no-default-browser-check',
|
||||
'--disable-default-apps',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-extensions',
|
||||
// AvoidUnnecessaryBeforeUnloadCheckSync - https://github.com/microsoft/playwright/issues/14047
|
||||
// Translate - https://github.com/microsoft/playwright/issues/16126
|
||||
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate',
|
||||
'--allow-pre-commit-input',
|
||||
'--disable-hang-monitor',
|
||||
'--disable-ipc-flooding-protection',
|
||||
'--disable-popup-blocking',
|
||||
'--disable-prompt-on-repost',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--disable-sync',
|
||||
'--force-color-profile=srgb',
|
||||
'--metrics-recording-only',
|
||||
'--no-first-run',
|
||||
'--enable-automation',
|
||||
'--password-store=basic',
|
||||
'--use-mock-keychain',
|
||||
// See https://chromium-review.googlesource.com/c/chromium/src/+/2436773
|
||||
'--no-service-autorun',
|
||||
'--export-tagged-pdf'
|
||||
];
|
||||
|
||||
async function urlToWSEndpoint(progress: Progress, endpointURL: string) {
|
||||
if (endpointURL.startsWith('ws'))
|
||||
return endpointURL;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
* Modifications 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.
|
||||
*/
|
||||
|
||||
// No dependencies as it is used from the Electron loader.
|
||||
|
||||
export const chromiumSwitches = [
|
||||
'--disable-field-trial-config', // https://source.chromium.org/chromium/chromium/src/+/main:testing/variations/README.md
|
||||
'--disable-background-networking',
|
||||
'--enable-features=NetworkService,NetworkServiceInProcess',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
'--disable-back-forward-cache', // Avoids surprises like main request not being intercepted during page.goBack().
|
||||
'--disable-breakpad',
|
||||
'--disable-client-side-phishing-detection',
|
||||
'--disable-component-extensions-with-background-pages',
|
||||
'--disable-component-update', // Avoids unneeded network activity after startup.
|
||||
'--no-default-browser-check',
|
||||
'--disable-default-apps',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-extensions',
|
||||
// AvoidUnnecessaryBeforeUnloadCheckSync - https://github.com/microsoft/playwright/issues/14047
|
||||
// Translate - https://github.com/microsoft/playwright/issues/16126
|
||||
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate',
|
||||
'--allow-pre-commit-input',
|
||||
'--disable-hang-monitor',
|
||||
'--disable-ipc-flooding-protection',
|
||||
'--disable-popup-blocking',
|
||||
'--disable-prompt-on-repost',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--disable-sync',
|
||||
'--force-color-profile=srgb',
|
||||
'--metrics-recording-only',
|
||||
'--no-first-run',
|
||||
'--enable-automation',
|
||||
'--password-store=basic',
|
||||
'--use-mock-keychain',
|
||||
// See https://chromium-review.googlesource.com/c/chromium/src/+/2436773
|
||||
'--no-service-autorun',
|
||||
'--export-tagged-pdf'
|
||||
];
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"Blackberry PlayBook": {
|
||||
"userAgent": "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/16.0 Safari/536.2+",
|
||||
"userAgent": "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/16.4 Safari/536.2+",
|
||||
"viewport": {
|
||||
"width": 600,
|
||||
"height": 1024
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Blackberry PlayBook landscape": {
|
||||
"userAgent": "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/16.0 Safari/536.2+",
|
||||
"userAgent": "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/16.4 Safari/536.2+",
|
||||
"viewport": {
|
||||
"width": 1024,
|
||||
"height": 600
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"BlackBerry Z30": {
|
||||
"userAgent": "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/16.0 Mobile Safari/537.10+",
|
||||
"userAgent": "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/16.4 Mobile Safari/537.10+",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"BlackBerry Z30 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/16.0 Mobile Safari/537.10+",
|
||||
"userAgent": "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/16.4 Mobile Safari/537.10+",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Galaxy Note 3": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/16.0 Mobile Safari/534.30",
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/16.4 Mobile Safari/534.30",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Galaxy Note 3 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/16.0 Mobile Safari/534.30",
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/16.4 Mobile Safari/534.30",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Galaxy Note II": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/16.0 Mobile Safari/534.30",
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/16.4 Mobile Safari/534.30",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Galaxy Note II landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/16.0 Mobile Safari/534.30",
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/16.4 Mobile Safari/534.30",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Galaxy S III": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/16.0 Mobile Safari/534.30",
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/16.4 Mobile Safari/534.30",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -99,7 +99,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Galaxy S III landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/16.0 Mobile Safari/534.30",
|
||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/16.4 Mobile Safari/534.30",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -198,7 +198,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"iPad (gen 6)": {
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 768,
|
||||
"height": 1024
|
||||
|
|
@ -209,7 +209,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPad (gen 6) landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 1024,
|
||||
"height": 768
|
||||
|
|
@ -220,7 +220,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPad (gen 7)": {
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 810,
|
||||
"height": 1080
|
||||
|
|
@ -231,7 +231,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPad (gen 7) landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 1080,
|
||||
"height": 810
|
||||
|
|
@ -242,7 +242,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPad Mini": {
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 768,
|
||||
"height": 1024
|
||||
|
|
@ -253,7 +253,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPad Mini landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 1024,
|
||||
"height": 768
|
||||
|
|
@ -264,7 +264,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPad Pro 11": {
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 834,
|
||||
"height": 1194
|
||||
|
|
@ -275,7 +275,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPad Pro 11 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 1194,
|
||||
"height": 834
|
||||
|
|
@ -286,7 +286,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 6": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.0 Mobile/15A372 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.4 Mobile/15A372 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 375,
|
||||
"height": 667
|
||||
|
|
@ -297,7 +297,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 6 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.0 Mobile/15A372 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.4 Mobile/15A372 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 667,
|
||||
"height": 375
|
||||
|
|
@ -308,7 +308,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 6 Plus": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.0 Mobile/15A372 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.4 Mobile/15A372 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 414,
|
||||
"height": 736
|
||||
|
|
@ -319,7 +319,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 6 Plus landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.0 Mobile/15A372 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.4 Mobile/15A372 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 736,
|
||||
"height": 414
|
||||
|
|
@ -330,7 +330,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 7": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.0 Mobile/15A372 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.4 Mobile/15A372 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 375,
|
||||
"height": 667
|
||||
|
|
@ -341,7 +341,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 7 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.0 Mobile/15A372 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.4 Mobile/15A372 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 667,
|
||||
"height": 375
|
||||
|
|
@ -352,7 +352,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 7 Plus": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.0 Mobile/15A372 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.4 Mobile/15A372 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 414,
|
||||
"height": 736
|
||||
|
|
@ -363,7 +363,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 7 Plus landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.0 Mobile/15A372 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.4 Mobile/15A372 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 736,
|
||||
"height": 414
|
||||
|
|
@ -374,7 +374,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 8": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.0 Mobile/15A372 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.4 Mobile/15A372 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 375,
|
||||
"height": 667
|
||||
|
|
@ -385,7 +385,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 8 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.0 Mobile/15A372 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.4 Mobile/15A372 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 667,
|
||||
"height": 375
|
||||
|
|
@ -396,7 +396,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 8 Plus": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.0 Mobile/15A372 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.4 Mobile/15A372 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 414,
|
||||
"height": 736
|
||||
|
|
@ -407,7 +407,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 8 Plus landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.0 Mobile/15A372 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.4 Mobile/15A372 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 736,
|
||||
"height": 414
|
||||
|
|
@ -418,7 +418,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone SE": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/16.0 Mobile/14E304 Safari/602.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/16.4 Mobile/14E304 Safari/602.1",
|
||||
"viewport": {
|
||||
"width": 320,
|
||||
"height": 568
|
||||
|
|
@ -429,7 +429,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone SE landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/16.0 Mobile/14E304 Safari/602.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/16.4 Mobile/14E304 Safari/602.1",
|
||||
"viewport": {
|
||||
"width": 568,
|
||||
"height": 320
|
||||
|
|
@ -440,7 +440,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone X": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.0 Mobile/15A372 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.4 Mobile/15A372 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 375,
|
||||
"height": 812
|
||||
|
|
@ -451,7 +451,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone X landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.0 Mobile/15A372 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/16.4 Mobile/15A372 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 812,
|
||||
"height": 375
|
||||
|
|
@ -462,7 +462,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone XR": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 414,
|
||||
"height": 896
|
||||
|
|
@ -473,7 +473,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone XR landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"viewport": {
|
||||
"width": 896,
|
||||
"height": 414
|
||||
|
|
@ -484,7 +484,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 11": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 414,
|
||||
"height": 896
|
||||
|
|
@ -499,7 +499,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 11 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 414,
|
||||
"height": 896
|
||||
|
|
@ -514,7 +514,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 11 Pro": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 375,
|
||||
"height": 812
|
||||
|
|
@ -529,7 +529,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 11 Pro landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 375,
|
||||
"height": 812
|
||||
|
|
@ -544,7 +544,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 11 Pro Max": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 414,
|
||||
"height": 896
|
||||
|
|
@ -559,7 +559,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 11 Pro Max landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 414,
|
||||
"height": 896
|
||||
|
|
@ -574,7 +574,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 12": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 390,
|
||||
"height": 844
|
||||
|
|
@ -589,7 +589,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 12 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 390,
|
||||
"height": 844
|
||||
|
|
@ -604,7 +604,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 12 Pro": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 390,
|
||||
"height": 844
|
||||
|
|
@ -619,7 +619,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 12 Pro landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 390,
|
||||
"height": 844
|
||||
|
|
@ -634,7 +634,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 12 Pro Max": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 428,
|
||||
"height": 926
|
||||
|
|
@ -649,7 +649,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 12 Pro Max landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 428,
|
||||
"height": 926
|
||||
|
|
@ -664,7 +664,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 12 Mini": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 375,
|
||||
"height": 812
|
||||
|
|
@ -679,7 +679,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 12 Mini landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 375,
|
||||
"height": 812
|
||||
|
|
@ -694,7 +694,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 13": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 390,
|
||||
"height": 844
|
||||
|
|
@ -709,7 +709,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 13 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 390,
|
||||
"height": 844
|
||||
|
|
@ -724,7 +724,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 13 Pro": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 390,
|
||||
"height": 844
|
||||
|
|
@ -739,7 +739,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 13 Pro landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 390,
|
||||
"height": 844
|
||||
|
|
@ -754,7 +754,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 13 Pro Max": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 428,
|
||||
"height": 926
|
||||
|
|
@ -769,7 +769,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 13 Pro Max landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 428,
|
||||
"height": 926
|
||||
|
|
@ -784,7 +784,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 13 Mini": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 375,
|
||||
"height": 812
|
||||
|
|
@ -799,7 +799,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"iPhone 13 Mini landscape": {
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1",
|
||||
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1",
|
||||
"screen": {
|
||||
"width": 375,
|
||||
"height": 812
|
||||
|
|
@ -1315,7 +1315,7 @@
|
|||
"defaultBrowserType": "firefox"
|
||||
},
|
||||
"Desktop Safari": {
|
||||
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15",
|
||||
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15",
|
||||
"screen": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
|
|||
|
|
@ -77,9 +77,14 @@ export class ElectronApplication extends SdkObject {
|
|||
});
|
||||
this._browserContext.setCustomCloseHandler(async () => {
|
||||
const electronHandle = await this._nodeElectronHandlePromise;
|
||||
await electronHandle.evaluate(({ app }) => app.quit());
|
||||
await electronHandle.evaluate(({ app }) => app.quit()).catch(() => {});
|
||||
});
|
||||
this._nodeSession.send('Runtime.enable', {}).catch(e => {});
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
await this._nodeSession.send('Runtime.enable', {});
|
||||
// Delay loading the app until browser is started and the browser targets are configured to auto-attach.
|
||||
await this._nodeSession.send('Runtime.evaluate', { expression: '__playwright_run()' });
|
||||
}
|
||||
|
||||
process(): childProcess.ChildProcess {
|
||||
|
|
@ -125,7 +130,7 @@ export class Electron extends SdkObject {
|
|||
controller.setLogName('browser');
|
||||
return controller.run(async progress => {
|
||||
let app: ElectronApplication | undefined = undefined;
|
||||
const electronArguments = [...args, '--inspect=0', '--remote-debugging-port=0'];
|
||||
const electronArguments = [require.resolve('./loader'), '--inspect=0', '--remote-debugging-port=0', options.cwd || process.cwd(), ...args];
|
||||
|
||||
if (os.platform() === 'linux') {
|
||||
const runningAsRoot = process.geteuid && process.geteuid() === 0;
|
||||
|
|
@ -231,6 +236,7 @@ export class Electron extends SdkObject {
|
|||
validateBrowserContextOptions(contextOptions, browserOptions);
|
||||
const browser = await CRBrowser.connect(chromeTransport, browserOptions);
|
||||
app = new ElectronApplication(this, browser, nodeConnection, launchedProcess);
|
||||
await app.initialize();
|
||||
return app;
|
||||
}, TimeoutSettings.timeout(options));
|
||||
}
|
||||
|
|
|
|||
57
packages/playwright-core/src/server/electron/loader.ts
Normal file
57
packages/playwright-core/src/server/electron/loader.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const { app } = require('electron');
|
||||
const path = require('path');
|
||||
const { chromiumSwitches } = require('../chromium/chromiumSwitches');
|
||||
|
||||
// Command line is like:
|
||||
// [Electron, loader.js, --inspect=0, --remote-debugging-port=0, options.cwd, app.js, ...args]
|
||||
const appPath = path.resolve(process.argv[4], process.argv[5]);
|
||||
process.argv.splice(2, 4);
|
||||
process.argv[1] = appPath;
|
||||
// Now it is like
|
||||
// [Electron, app.js, ...args]
|
||||
|
||||
for (const arg of chromiumSwitches) {
|
||||
const match = arg.match(/--([^=]*)=?(.*)/)!;
|
||||
app.commandLine.appendSwitch(match[1], match[2]);
|
||||
}
|
||||
|
||||
app.getAppPath = () => path.dirname(appPath);
|
||||
|
||||
let launchInfoEventPayload: any;
|
||||
app.on('ready', launchInfo => launchInfoEventPayload = launchInfo);
|
||||
|
||||
(globalThis as any).__playwright_run = async () => {
|
||||
// Wait for app to be ready to avoid browser initialization races.
|
||||
await app.whenReady();
|
||||
|
||||
// Override isReady pipeline.
|
||||
let isReady = false;
|
||||
let whenReadyCallback: () => void;
|
||||
const whenReadyPromise = new Promise<void>(f => whenReadyCallback = f);
|
||||
app.isReady = () => isReady;
|
||||
app.whenReady = () => whenReadyPromise;
|
||||
|
||||
require(appPath);
|
||||
|
||||
// Trigger isReady.
|
||||
isReady = true;
|
||||
whenReadyCallback!();
|
||||
app.emit('will-finish-launching');
|
||||
app.emit('ready', launchInfoEventPayload);
|
||||
};
|
||||
|
|
@ -78,6 +78,7 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
readonly fetchResponses: Map<string, Buffer> = new Map();
|
||||
readonly fetchLog: Map<string, string[]> = new Map();
|
||||
protected static allInstances: Set<APIRequestContext> = new Set();
|
||||
readonly _activeProgressControllers = new Set<ProgressController>();
|
||||
|
||||
static findResponseBody(guid: string): Buffer | undefined {
|
||||
for (const request of APIRequestContext.allInstances) {
|
||||
|
|
|
|||
|
|
@ -1667,7 +1667,7 @@ export class Frame extends SdkObject {
|
|||
for (let i = 0; i < frameChunks.length - 1 && progress.isRunning(); ++i) {
|
||||
const info = this._page.parseSelector(frameChunks[i], options);
|
||||
const task = dom.waitForSelectorTask(info, 'attached', false, i === 0 ? scope : undefined);
|
||||
progress.log(` waiting for frameLocator('${stringifySelector(frameChunks[i])}')`);
|
||||
progress.log(` waiting for ${this._asLocator(stringifySelector(frameChunks[i]) + ' >> internal:control=enter-frame')}`);
|
||||
const handle = i === 0 && scope ? await frame._runWaitForSelectorTaskOnce(progress, stringifySelector(info.parsed), info.world, task)
|
||||
: await frame._scheduleRerunnableHandleTask(progress, info.world, task);
|
||||
const element = handle.asElement() as dom.ElementHandle<Element>;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import type { SelectorEngine, SelectorRoot } from './selectorEngine';
|
|||
import { XPathEngine } from './xpathSelectorEngine';
|
||||
import { ReactEngine } from './reactSelectorEngine';
|
||||
import { VueEngine } from './vueSelectorEngine';
|
||||
import { RoleEngine } from './roleSelectorEngine';
|
||||
import { createRoleEngine } from './roleSelectorEngine';
|
||||
import { parseAttributeSelector } from '../isomorphic/selectorParser';
|
||||
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../isomorphic/selectorParser';
|
||||
import { allEngineNames, parseSelector, stringifySelector } from '../isomorphic/selectorParser';
|
||||
|
|
@ -95,7 +95,7 @@ export class InjectedScript {
|
|||
this._engines.set('xpath:light', XPathEngine);
|
||||
this._engines.set('_react', ReactEngine);
|
||||
this._engines.set('_vue', VueEngine);
|
||||
this._engines.set('role', RoleEngine);
|
||||
this._engines.set('role', createRoleEngine(false));
|
||||
this._engines.set('text', this._createTextEngine(true, false));
|
||||
this._engines.set('text:light', this._createTextEngine(false, false));
|
||||
this._engines.set('id', this._createAttributeEngine('id', true));
|
||||
|
|
@ -116,7 +116,7 @@ export class InjectedScript {
|
|||
this._engines.set('internal:has-text', this._createInternalHasTextEngine());
|
||||
this._engines.set('internal:attr', this._createNamedAttributeEngine());
|
||||
this._engines.set('internal:testid', this._createNamedAttributeEngine());
|
||||
this._engines.set('internal:role', RoleEngine);
|
||||
this._engines.set('internal:role', createRoleEngine(true));
|
||||
|
||||
for (const { name, engine } of customEngines)
|
||||
this._engines.set(name, engine);
|
||||
|
|
|
|||
|
|
@ -75,8 +75,10 @@ class Recorder {
|
|||
addEventListener(document, 'mouseup', event => this._onMouseUp(event as MouseEvent), true),
|
||||
addEventListener(document, 'mousemove', event => this._onMouseMove(event as MouseEvent), true),
|
||||
addEventListener(document, 'mouseleave', event => this._onMouseLeave(event as MouseEvent), true),
|
||||
addEventListener(document, 'focus', () => this._onFocus(true), true),
|
||||
addEventListener(document, 'scroll', () => {
|
||||
addEventListener(document, 'focus', event => event.isTrusted && this._onFocus(true), true),
|
||||
addEventListener(document, 'scroll', event => {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
this._hoveredModel = null;
|
||||
this._highlight.hideActionPoint();
|
||||
this._updateHighlight();
|
||||
|
|
@ -156,6 +158,8 @@ class Recorder {
|
|||
}
|
||||
|
||||
private _onClick(event: MouseEvent) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
if (this._mode === 'inspecting')
|
||||
globalThis.__pw_recorderSetSelector(this._hoveredModel ? this._hoveredModel.selector : '');
|
||||
if (this._shouldIgnoreMouseEvent(event))
|
||||
|
|
@ -204,6 +208,8 @@ class Recorder {
|
|||
}
|
||||
|
||||
private _onMouseDown(event: MouseEvent) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
if (this._shouldIgnoreMouseEvent(event))
|
||||
return;
|
||||
if (!this._performingAction)
|
||||
|
|
@ -212,6 +218,8 @@ class Recorder {
|
|||
}
|
||||
|
||||
private _onMouseUp(event: MouseEvent) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
if (this._shouldIgnoreMouseEvent(event))
|
||||
return;
|
||||
if (!this._performingAction)
|
||||
|
|
@ -219,6 +227,8 @@ class Recorder {
|
|||
}
|
||||
|
||||
private _onMouseMove(event: MouseEvent) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
if (this._mode === 'none')
|
||||
return;
|
||||
const target = this._deepEventTarget(event);
|
||||
|
|
@ -229,6 +239,8 @@ class Recorder {
|
|||
}
|
||||
|
||||
private _onMouseLeave(event: MouseEvent) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
// Leaving iframe.
|
||||
if (window.top !== window && this._deepEventTarget(event).nodeType === Node.DOCUMENT_NODE) {
|
||||
this._hoveredElement = null;
|
||||
|
|
@ -339,6 +351,8 @@ class Recorder {
|
|||
}
|
||||
|
||||
private _onKeyDown(event: KeyboardEvent) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
if (this._mode === 'inspecting') {
|
||||
consumeEvent(event);
|
||||
return;
|
||||
|
|
@ -376,6 +390,8 @@ class Recorder {
|
|||
}
|
||||
|
||||
private _onKeyUp(event: KeyboardEvent) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
if (this._mode === 'none')
|
||||
return;
|
||||
if (!this._shouldGenerateKeyPressFor(event))
|
||||
|
|
|
|||
|
|
@ -72,6 +72,11 @@ function validateAttributes(attrs: AttributeSelectorPart[], role: string) {
|
|||
validateSupportedRole(attr.name, kAriaExpandedRoles, role);
|
||||
validateSupportedValues(attr, [true, false]);
|
||||
validateSupportedOp(attr, ['<truthy>', '=']);
|
||||
if (attr.op === '<truthy>') {
|
||||
// Do not match "none" in "treeitem[expanded]".
|
||||
attr.op = '=';
|
||||
attr.value = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'level': {
|
||||
|
|
@ -107,8 +112,8 @@ function validateAttributes(attrs: AttributeSelectorPart[], role: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export const RoleEngine: SelectorEngine = {
|
||||
queryAll(scope: SelectorRoot, selector: string): Element[] {
|
||||
export function createRoleEngine(internal: boolean): SelectorEngine {
|
||||
const queryAll = (scope: SelectorRoot, selector: string): Element[] => {
|
||||
const parsed = parseAttributeSelector(selector, true);
|
||||
const role = parsed.name.toLowerCase();
|
||||
if (!role)
|
||||
|
|
@ -149,7 +154,13 @@ export const RoleEngine: SelectorEngine = {
|
|||
return;
|
||||
}
|
||||
if (nameAttr !== undefined) {
|
||||
const accessibleName = getElementAccessibleName(element, includeHidden, hiddenCache);
|
||||
// Always normalize whitespace in the accessible name.
|
||||
const accessibleName = getElementAccessibleName(element, includeHidden, hiddenCache).trim().replace(/\s+/g, ' ');
|
||||
if (typeof nameAttr.value === 'string')
|
||||
nameAttr.value = nameAttr.value.trim().replace(/\s+/g, ' ');
|
||||
// internal:role assumes that [name="foo"i] also means substring.
|
||||
if (internal && !nameAttr.caseSensitive && nameAttr.op === '=')
|
||||
nameAttr.op = '*=';
|
||||
if (!matchesAttributePart(accessibleName, nameAttr))
|
||||
return;
|
||||
}
|
||||
|
|
@ -170,5 +181,6 @@ export const RoleEngine: SelectorEngine = {
|
|||
|
||||
query(scope);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
return { queryAll };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -670,15 +670,21 @@ export function getAriaPressed(element: Element): boolean | 'mixed' {
|
|||
}
|
||||
|
||||
export const kAriaExpandedRoles = ['application', 'button', 'checkbox', 'combobox', 'gridcell', 'link', 'listbox', 'menuitem', 'row', 'rowheader', 'tab', 'treeitem', 'columnheader', 'menuitemcheckbox', 'menuitemradio', 'rowheader', 'switch'];
|
||||
export function getAriaExpanded(element: Element): boolean {
|
||||
export function getAriaExpanded(element: Element): boolean | 'none' {
|
||||
// https://www.w3.org/TR/wai-aria-1.2/#aria-expanded
|
||||
// https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
|
||||
if (element.tagName === 'DETAILS')
|
||||
return (element as HTMLDetailsElement).open;
|
||||
if (kAriaExpandedRoles.includes(getAriaRole(element) || ''))
|
||||
return getAriaBoolean(element.getAttribute('aria-expanded')) === true;
|
||||
if (kAriaExpandedRoles.includes(getAriaRole(element) || '')) {
|
||||
const expanded = element.getAttribute('aria-expanded');
|
||||
if (expanded === null)
|
||||
return 'none';
|
||||
if (expanded === 'true')
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
return 'none';
|
||||
}
|
||||
|
||||
export const kAriaLevelRoles = ['heading', 'listitem', 'row', 'treeitem'];
|
||||
export function getAriaLevel(element: Element): number {
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ function buildCandidates(injectedScript: InjectedScript, element: Element, testI
|
|||
if (ariaRole && !['none', 'presentation'].includes(ariaRole)) {
|
||||
const ariaName = getElementAccessibleName(element, false, accessibleNameCache);
|
||||
if (ariaName)
|
||||
candidates.push({ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScore });
|
||||
candidates.push({ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, false)}]`, score: kRoleWithNameScore });
|
||||
else
|
||||
candidates.push({ engine: 'internal:role', selector: ariaRole, score: kRoleWithoutNameScore });
|
||||
}
|
||||
|
|
@ -227,7 +227,7 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
|
|||
if (ariaRole && !['none', 'presentation'].includes(ariaRole)) {
|
||||
const ariaName = getElementAccessibleName(element, false, accessibleNameCache);
|
||||
if (ariaName)
|
||||
candidate.push({ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScore });
|
||||
candidate.push({ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, false)}]`, score: kRoleWithNameScore });
|
||||
else
|
||||
candidate.push({ engine: 'internal:role', selector: ariaRole, score: kRoleWithoutNameScore });
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ export type Language = 'javascript' | 'python' | 'java' | 'csharp';
|
|||
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has' | 'frame';
|
||||
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
|
||||
|
||||
type LocatorOptions = { attrs?: { name: string, value: string | boolean | number}[], exact?: boolean, name?: string | RegExp };
|
||||
export interface LocatorFactory {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options?: { attrs?: Record<string, string | boolean>, exact?: boolean }): string;
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options?: LocatorOptions): string;
|
||||
}
|
||||
|
||||
export function asLocator(lang: Language, selector: string, isFrameLocator: boolean = false): string {
|
||||
|
|
@ -31,10 +32,21 @@ export function asLocator(lang: Language, selector: string, isFrameLocator: bool
|
|||
}
|
||||
|
||||
function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrameLocator: boolean = false): string {
|
||||
const parts = [...parsed.parts];
|
||||
// frameLocator('iframe').first is actually "iframe >> nth=0 >> internal:control=enter-frame"
|
||||
// To make it easier to parse, we turn it into "iframe >> internal:control=enter-frame >> nth=0"
|
||||
for (let index = 0; index < parts.length - 1; index++) {
|
||||
if (parts[index].name === 'nth' && parts[index + 1].name === 'internal:control' && (parts[index + 1].body as string) === 'enter-frame') {
|
||||
// Swap nth and enter-frame.
|
||||
const [nth] = parts.splice(index, 1);
|
||||
parts.splice(index + 1, 0, nth);
|
||||
}
|
||||
}
|
||||
|
||||
const tokens: string[] = [];
|
||||
let nextBase: LocatorBase = isFrameLocator ? 'frame-locator' : 'page';
|
||||
for (let index = 0; index < parsed.parts.length; index++) {
|
||||
const part = parsed.parts[index];
|
||||
for (let index = 0; index < parts.length; index++) {
|
||||
const part = parts[index];
|
||||
const base = nextBase;
|
||||
nextBase = 'locator';
|
||||
|
||||
|
|
@ -69,10 +81,18 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
|||
}
|
||||
if (part.name === 'internal:role') {
|
||||
const attrSelector = parseAttributeSelector(part.body as string, true);
|
||||
const attrs: Record<string, boolean | string> = {};
|
||||
for (const attr of attrSelector.attributes!)
|
||||
attrs[attr.name === 'include-hidden' ? 'includeHidden' : attr.name] = attr.value;
|
||||
tokens.push(factory.generateLocator(base, 'role', attrSelector.name, { attrs }));
|
||||
const options: LocatorOptions = { attrs: [] };
|
||||
for (const attr of attrSelector.attributes) {
|
||||
if (attr.name === 'name') {
|
||||
options.exact = attr.caseSensitive;
|
||||
options.name = attr.value;
|
||||
} else {
|
||||
if (attr.name === 'level' && typeof attr.value === 'string')
|
||||
attr.value = +attr.value;
|
||||
options.attrs!.push({ name: attr.name === 'include-hidden' ? 'includeHidden' : attr.name, value: attr.value });
|
||||
}
|
||||
}
|
||||
tokens.push(factory.generateLocator(base, 'role', attrSelector.name, options));
|
||||
continue;
|
||||
}
|
||||
if (part.name === 'internal:testid') {
|
||||
|
|
@ -102,7 +122,7 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
|||
|
||||
let locatorType: LocatorType = 'default';
|
||||
|
||||
const nextPart = parsed.parts[index + 1];
|
||||
const nextPart = parts[index + 1];
|
||||
if (nextPart && nextPart.name === 'internal:control' && (nextPart.body as string) === 'enter-frame') {
|
||||
locatorType = 'frame';
|
||||
nextBase = 'frame-locator';
|
||||
|
|
@ -134,7 +154,7 @@ function detectExact(text: string): { exact?: boolean, text: string | RegExp } {
|
|||
}
|
||||
|
||||
export class JavaScriptLocatorFactory implements LocatorFactory {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: { attrs?: Record<string, string | boolean>, exact?: boolean } = {}): string {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: LocatorOptions = {}): string {
|
||||
switch (kind) {
|
||||
case 'default':
|
||||
return `locator(${this.quote(body as string)})`;
|
||||
|
|
@ -148,7 +168,14 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
|||
return `last()`;
|
||||
case 'role':
|
||||
const attrs: string[] = [];
|
||||
for (const [name, value] of Object.entries(options.attrs!))
|
||||
if (isRegExp(options.name)) {
|
||||
attrs.push(`name: ${options.name}`);
|
||||
} else if (typeof options.name === 'string') {
|
||||
attrs.push(`name: ${this.quote(options.name)}`);
|
||||
if (options.exact)
|
||||
attrs.push(`exact: true`);
|
||||
}
|
||||
for (const { name, value } of options.attrs!)
|
||||
attrs.push(`${name}: ${typeof value === 'string' ? this.quote(value) : value}`);
|
||||
const attrString = attrs.length ? `, { ${attrs.join(', ')} }` : '';
|
||||
return `getByRole(${this.quote(body as string)}${attrString})`;
|
||||
|
|
@ -191,7 +218,7 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
|
||||
export class PythonLocatorFactory implements LocatorFactory {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: { attrs?: Record<string, string | boolean>, exact?: boolean } = {}): string {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: LocatorOptions = {}): string {
|
||||
switch (kind) {
|
||||
case 'default':
|
||||
return `locator(${this.quote(body as string)})`;
|
||||
|
|
@ -205,8 +232,19 @@ export class PythonLocatorFactory implements LocatorFactory {
|
|||
return `last`;
|
||||
case 'role':
|
||||
const attrs: string[] = [];
|
||||
for (const [name, value] of Object.entries(options.attrs!))
|
||||
attrs.push(`${toSnakeCase(name)}=${typeof value === 'string' ? this.quote(value) : value}`);
|
||||
if (isRegExp(options.name)) {
|
||||
attrs.push(`name=${this.regexToString(options.name)}`);
|
||||
} else if (typeof options.name === 'string') {
|
||||
attrs.push(`name=${this.quote(options.name)}`);
|
||||
if (options.exact)
|
||||
attrs.push(`exact=True`);
|
||||
}
|
||||
for (const { name, value } of options.attrs!) {
|
||||
let valueString = typeof value === 'string' ? this.quote(value) : value;
|
||||
if (typeof value === 'boolean')
|
||||
valueString = value ? 'True' : 'False';
|
||||
attrs.push(`${toSnakeCase(name)}=${valueString}`);
|
||||
}
|
||||
const attrString = attrs.length ? `, ${attrs.join(', ')}` : '';
|
||||
return `get_by_role(${this.quote(body as string)}${attrString})`;
|
||||
case 'has-text':
|
||||
|
|
@ -230,21 +268,22 @@ export class PythonLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private toCallWithExact(method: string, body: string | RegExp, exact: boolean) {
|
||||
if (isRegExp(body)) {
|
||||
private regexToString(body: RegExp) {
|
||||
const suffix = body.flags.includes('i') ? ', re.IGNORECASE' : '';
|
||||
return `${method}(re.compile(r"${body.source.replace(/\\\//, '/').replace(/"/g, '\\"')}"${suffix}))`;
|
||||
return `re.compile(r"${body.source.replace(/\\\//, '/').replace(/"/g, '\\"')}"${suffix})`;
|
||||
}
|
||||
|
||||
private toCallWithExact(method: string, body: string | RegExp, exact: boolean) {
|
||||
if (isRegExp(body))
|
||||
return `${method}(${this.regexToString(body)})`;
|
||||
if (exact)
|
||||
return `${method}(${this.quote(body)}, exact=True)`;
|
||||
return `${method}(${this.quote(body)})`;
|
||||
}
|
||||
|
||||
private toHasText(body: string | RegExp) {
|
||||
if (isRegExp(body)) {
|
||||
const suffix = body.flags.includes('i') ? ', re.IGNORECASE' : '';
|
||||
return `re.compile(r"${body.source.replace(/\\\//, '/').replace(/"/g, '\\"')}"${suffix})`;
|
||||
}
|
||||
if (isRegExp(body))
|
||||
return this.regexToString(body);
|
||||
return `${this.quote(body)}`;
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +293,7 @@ export class PythonLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
|
||||
export class JavaLocatorFactory implements LocatorFactory {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: { attrs?: Record<string, string | boolean>, exact?: boolean } = {}): string {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: LocatorOptions = {}): string {
|
||||
let clazz: string;
|
||||
switch (base) {
|
||||
case 'page': clazz = 'Page'; break;
|
||||
|
|
@ -274,7 +313,14 @@ export class JavaLocatorFactory implements LocatorFactory {
|
|||
return `last()`;
|
||||
case 'role':
|
||||
const attrs: string[] = [];
|
||||
for (const [name, value] of Object.entries(options.attrs!))
|
||||
if (isRegExp(options.name)) {
|
||||
attrs.push(`.setName(${this.regexToString(options.name)})`);
|
||||
} else if (typeof options.name === 'string') {
|
||||
attrs.push(`.setName(${this.quote(options.name)})`);
|
||||
if (options.exact)
|
||||
attrs.push(`.setExact(true)`);
|
||||
}
|
||||
for (const { name, value } of options.attrs!)
|
||||
attrs.push(`.set${toTitleCase(name)}(${typeof value === 'string' ? this.quote(value) : value})`);
|
||||
const attrString = attrs.length ? `, new ${clazz}.GetByRoleOptions()${attrs.join('')}` : '';
|
||||
return `getByRole(AriaRole.${toSnakeCase(body as string).toUpperCase()}${attrString})`;
|
||||
|
|
@ -299,21 +345,22 @@ export class JavaLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private toCallWithExact(clazz: string, method: string, body: string | RegExp, exact: boolean) {
|
||||
if (isRegExp(body)) {
|
||||
private regexToString(body: RegExp) {
|
||||
const suffix = body.flags.includes('i') ? ', Pattern.CASE_INSENSITIVE' : '';
|
||||
return `${method}(Pattern.compile(${this.quote(body.source)}${suffix}))`;
|
||||
return `Pattern.compile(${this.quote(body.source)}${suffix})`;
|
||||
}
|
||||
|
||||
private toCallWithExact(clazz: string, method: string, body: string | RegExp, exact: boolean) {
|
||||
if (isRegExp(body))
|
||||
return `${method}(${this.regexToString(body)})`;
|
||||
if (exact)
|
||||
return `${method}(${this.quote(body)}, new ${clazz}.${toTitleCase(method)}Options().setExact(true))`;
|
||||
return `${method}(${this.quote(body)})`;
|
||||
}
|
||||
|
||||
private toHasText(body: string | RegExp) {
|
||||
if (isRegExp(body)) {
|
||||
const suffix = body.flags.includes('i') ? ', Pattern.CASE_INSENSITIVE' : '';
|
||||
return `Pattern.compile(${this.quote(body.source)}${suffix})`;
|
||||
}
|
||||
if (isRegExp(body))
|
||||
return this.regexToString(body);
|
||||
return this.quote(body);
|
||||
}
|
||||
|
||||
|
|
@ -323,7 +370,7 @@ export class JavaLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
|
||||
export class CSharpLocatorFactory implements LocatorFactory {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: { attrs?: Record<string, string | boolean>, exact?: boolean } = {}): string {
|
||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: LocatorOptions = {}): string {
|
||||
switch (kind) {
|
||||
case 'default':
|
||||
return `Locator(${this.quote(body as string)})`;
|
||||
|
|
@ -337,14 +384,19 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
|||
return `Last`;
|
||||
case 'role':
|
||||
const attrs: string[] = [];
|
||||
for (const [name, value] of Object.entries(options.attrs!)) {
|
||||
const optionKey = name === 'name' ? 'NameString' : toTitleCase(name);
|
||||
attrs.push(`${optionKey} = ${typeof value === 'string' ? this.quote(value) : value}`);
|
||||
if (isRegExp(options.name)) {
|
||||
attrs.push(`NameRegex = ${this.regexToString(options.name)}`);
|
||||
} else if (typeof options.name === 'string') {
|
||||
attrs.push(`NameString = ${this.quote(options.name)}`);
|
||||
if (options.exact)
|
||||
attrs.push(`Exact = true`);
|
||||
}
|
||||
for (const { name, value } of options.attrs!)
|
||||
attrs.push(`${toTitleCase(name)} = ${typeof value === 'string' ? this.quote(value) : value}`);
|
||||
const attrString = attrs.length ? `, new() { ${attrs.join(', ')} }` : '';
|
||||
return `GetByRole(AriaRole.${toTitleCase(body as string)}${attrString})`;
|
||||
case 'has-text':
|
||||
return `Filter(new() { HasTextString = ${this.toHasText(body)} })`;
|
||||
return `Filter(new() { ${this.toHasText(body)} })`;
|
||||
case 'has':
|
||||
return `Filter(new() { Has = ${body} })`;
|
||||
case 'test-id':
|
||||
|
|
@ -364,22 +416,23 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private toCallWithExact(method: string, body: string | RegExp, exact: boolean) {
|
||||
if (isRegExp(body)) {
|
||||
private regexToString(body: RegExp): string {
|
||||
const suffix = body.flags.includes('i') ? ', RegexOptions.IgnoreCase' : '';
|
||||
return `${method}(new Regex(${this.quote(body.source)}${suffix}))`;
|
||||
return `new Regex(${this.quote(body.source)}${suffix})`;
|
||||
}
|
||||
|
||||
private toCallWithExact(method: string, body: string | RegExp, exact: boolean) {
|
||||
if (isRegExp(body))
|
||||
return `${method}(${this.regexToString(body)})`;
|
||||
if (exact)
|
||||
return `${method}(${this.quote(body)}, new() { Exact = true })`;
|
||||
return `${method}(${this.quote(body)})`;
|
||||
}
|
||||
|
||||
private toHasText(body: string | RegExp) {
|
||||
if (isRegExp(body)) {
|
||||
const suffix = body.flags.includes('i') ? ', RegexOptions.IgnoreCase' : '';
|
||||
return `new Regex(${this.quote(body.source)}${suffix})`;
|
||||
}
|
||||
return this.quote(body);
|
||||
if (isRegExp(body))
|
||||
return `HasTextRegex = ${this.regexToString(body)}`;
|
||||
return `HasTextString = ${this.quote(body)}`;
|
||||
}
|
||||
|
||||
private quote(text: string) {
|
||||
|
|
|
|||
|
|
@ -152,8 +152,19 @@ function transform(template: string, params: TemplateParams, testIdAttributeName
|
|||
.replace(/,exact=true/g, 's')
|
||||
.replace(/\,/g, '][');
|
||||
|
||||
const parts = template.split('.');
|
||||
// Turn "internal:control=enter-frame >> nth=0" into "nth=0 >> internal:control=enter-frame"
|
||||
// because these are swapped in locators vs selectors.
|
||||
for (let index = 0; index < parts.length - 1; index++) {
|
||||
if (parts[index] === 'internal:control=enter-frame' && parts[index + 1].startsWith('nth=')) {
|
||||
// Swap nth and enter-frame.
|
||||
const [nth] = parts.splice(index, 1);
|
||||
parts.splice(index + 1, 0, nth);
|
||||
}
|
||||
}
|
||||
|
||||
// Substitute params.
|
||||
return template.split('.').map(t => {
|
||||
return parts.map(t => {
|
||||
if (!t.startsWith('internal:') || t === 'internal:control')
|
||||
return t.replace(/\$(\d+)/g, (_, ordinal) => { const param = params[+ordinal - 1]; return param.text; });
|
||||
t = t.includes('[') ? t.replace(/\]/, '') + ']' : t;
|
||||
|
|
@ -188,7 +199,7 @@ export function locatorOrSelectorAsSelector(language: Language, locator: string,
|
|||
return selector;
|
||||
} catch (e) {
|
||||
}
|
||||
return locator;
|
||||
return '';
|
||||
}
|
||||
|
||||
function digestForComparison(locator: string) {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ export class ProgressController {
|
|||
return this._lastIntermediateResult;
|
||||
}
|
||||
|
||||
abort(error: Error) {
|
||||
this._forceAbortPromise.reject(error);
|
||||
}
|
||||
|
||||
async run<T>(task: (progress: Progress) => Promise<T>, timeout?: number): Promise<T> {
|
||||
if (timeout) {
|
||||
this._timeout = timeout;
|
||||
|
|
@ -71,6 +75,7 @@ export class ProgressController {
|
|||
|
||||
assert(this._state === 'before');
|
||||
this._state = 'running';
|
||||
this.sdkObject.attribution.context?._activeProgressControllers.add(this);
|
||||
|
||||
const progress: Progress = {
|
||||
log: message => {
|
||||
|
|
@ -117,6 +122,7 @@ export class ProgressController {
|
|||
await Promise.all(this._cleanups.splice(0).map(runCleanup));
|
||||
throw e;
|
||||
} finally {
|
||||
this.sdkObject.attribution.context?._activeProgressControllers.delete(this);
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ import { kPageProxyMessageReceived, WKConnection, WKSession } from './wkConnecti
|
|||
import { WKPage } from './wkPage';
|
||||
import { kBrowserClosedError } from '../../common/errors';
|
||||
|
||||
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15';
|
||||
const BROWSER_VERSION = '16.0';
|
||||
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15';
|
||||
const BROWSER_VERSION = '16.4';
|
||||
|
||||
export class WKBrowser extends Browser {
|
||||
private readonly _connection: WKConnection;
|
||||
|
|
|
|||
|
|
@ -191,10 +191,12 @@ export function constructURLBasedOnBaseURL(baseURL: string | undefined, givenURL
|
|||
|
||||
export function wrapInASCIIBox(text: string, padding = 0): string {
|
||||
const lines = text.split('\n');
|
||||
const maxLineLength = Math.max(...lines.map(line => line.length));
|
||||
const separatorLength = process.stdout.columns || maxLineLength;
|
||||
const separator = '═'.repeat(separatorLength);
|
||||
return [separator, ...lines, separator, ''].join('\n');
|
||||
const maxLength = Math.max(...lines.map(line => line.length));
|
||||
return [
|
||||
'╔' + '═'.repeat(maxLength + padding * 2) + '╗',
|
||||
...lines.map(line => '║' + ' '.repeat(padding) + line + ' '.repeat(maxLength - line.length + padding) + '║'),
|
||||
'╚' + '═'.repeat(maxLength + padding * 2) + '╝',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function isFilePayload(value: any): boolean {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { escapeForAttributeSelector, escapeForTextSelector, isString } from './s
|
|||
export type ByRoleOptions = {
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
exact?: boolean;
|
||||
expanded?: boolean;
|
||||
includeHidden?: boolean;
|
||||
level?: number;
|
||||
|
|
@ -72,7 +73,7 @@ export function getByRoleSelector(role: string, options: ByRoleOptions = {}): st
|
|||
if (options.level !== undefined)
|
||||
props.push(['level', String(options.level)]);
|
||||
if (options.name !== undefined)
|
||||
props.push(['name', isString(options.name) ? escapeForAttributeSelector(options.name, false) : String(options.name)]);
|
||||
props.push(['name', isString(options.name) ? escapeForAttributeSelector(options.name, !!options.exact) : String(options.name)]);
|
||||
if (options.pressed !== undefined)
|
||||
props.push(['pressed', String(options.pressed)]);
|
||||
return `internal:role=${role}${props.map(([n, v]) => `[${n}=${v}]`).join('')}`;
|
||||
|
|
|
|||
90
packages/playwright-core/types/types.d.ts
vendored
90
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -2517,15 +2517,14 @@ export interface Page {
|
|||
*/
|
||||
getByRole(role: "alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"|"caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"|"deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"|"group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"|"menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"|"presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|"searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"|"tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem", options?: {
|
||||
/**
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for
|
||||
* checked are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
|
||||
*
|
||||
* Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked).
|
||||
*/
|
||||
checked?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
* An attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
*
|
||||
* > NOTE: Unlike most other attributes, `disabled` is inherited through the DOM hierarchy. Learn more about
|
||||
* [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled).
|
||||
|
|
@ -2533,14 +2532,20 @@ export interface Page {
|
|||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-expanded`.
|
||||
* Whether `name` is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when `name` is a regular
|
||||
* expression. Note that exact match still trims whitespace.
|
||||
*/
|
||||
exact?: boolean;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-expanded`.
|
||||
*
|
||||
* Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded).
|
||||
*/
|
||||
expanded?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* Option that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector.
|
||||
*
|
||||
* Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden).
|
||||
|
|
@ -2556,21 +2561,22 @@ export interface Page {
|
|||
level?: number;
|
||||
|
||||
/**
|
||||
* A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
* Option to match the [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). By default, matching is
|
||||
* case-insensitive and searches for a substring, use `exact` to control this behavior.
|
||||
*
|
||||
* Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
*/
|
||||
name?: string|RegExp;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-pressed`.
|
||||
*
|
||||
* Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed).
|
||||
*/
|
||||
pressed?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-selected`.
|
||||
* An attribute that is usually set by `aria-selected`.
|
||||
*
|
||||
* Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected).
|
||||
*/
|
||||
|
|
@ -5657,15 +5663,14 @@ export interface Frame {
|
|||
*/
|
||||
getByRole(role: "alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"|"caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"|"deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"|"group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"|"menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"|"presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|"searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"|"tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem", options?: {
|
||||
/**
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for
|
||||
* checked are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
|
||||
*
|
||||
* Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked).
|
||||
*/
|
||||
checked?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
* An attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
*
|
||||
* > NOTE: Unlike most other attributes, `disabled` is inherited through the DOM hierarchy. Learn more about
|
||||
* [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled).
|
||||
|
|
@ -5673,14 +5678,20 @@ export interface Frame {
|
|||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-expanded`.
|
||||
* Whether `name` is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when `name` is a regular
|
||||
* expression. Note that exact match still trims whitespace.
|
||||
*/
|
||||
exact?: boolean;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-expanded`.
|
||||
*
|
||||
* Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded).
|
||||
*/
|
||||
expanded?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* Option that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector.
|
||||
*
|
||||
* Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden).
|
||||
|
|
@ -5696,21 +5707,22 @@ export interface Frame {
|
|||
level?: number;
|
||||
|
||||
/**
|
||||
* A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
* Option to match the [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). By default, matching is
|
||||
* case-insensitive and searches for a substring, use `exact` to control this behavior.
|
||||
*
|
||||
* Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
*/
|
||||
name?: string|RegExp;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-pressed`.
|
||||
*
|
||||
* Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed).
|
||||
*/
|
||||
pressed?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-selected`.
|
||||
* An attribute that is usually set by `aria-selected`.
|
||||
*
|
||||
* Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected).
|
||||
*/
|
||||
|
|
@ -10187,15 +10199,14 @@ export interface Locator {
|
|||
*/
|
||||
getByRole(role: "alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"|"caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"|"deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"|"group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"|"menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"|"presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|"searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"|"tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem", options?: {
|
||||
/**
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for
|
||||
* checked are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
|
||||
*
|
||||
* Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked).
|
||||
*/
|
||||
checked?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
* An attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
*
|
||||
* > NOTE: Unlike most other attributes, `disabled` is inherited through the DOM hierarchy. Learn more about
|
||||
* [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled).
|
||||
|
|
@ -10203,14 +10214,20 @@ export interface Locator {
|
|||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-expanded`.
|
||||
* Whether `name` is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when `name` is a regular
|
||||
* expression. Note that exact match still trims whitespace.
|
||||
*/
|
||||
exact?: boolean;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-expanded`.
|
||||
*
|
||||
* Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded).
|
||||
*/
|
||||
expanded?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* Option that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector.
|
||||
*
|
||||
* Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden).
|
||||
|
|
@ -10226,21 +10243,22 @@ export interface Locator {
|
|||
level?: number;
|
||||
|
||||
/**
|
||||
* A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
* Option to match the [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). By default, matching is
|
||||
* case-insensitive and searches for a substring, use `exact` to control this behavior.
|
||||
*
|
||||
* Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
*/
|
||||
name?: string|RegExp;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-pressed`.
|
||||
*
|
||||
* Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed).
|
||||
*/
|
||||
pressed?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-selected`.
|
||||
* An attribute that is usually set by `aria-selected`.
|
||||
*
|
||||
* Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected).
|
||||
*/
|
||||
|
|
@ -13483,7 +13501,6 @@ export interface APIRequest {
|
|||
* If you want API requests to not interfere with the browser cookies you should create a new [APIRequestContext] by
|
||||
* calling [apiRequest.newContext([options])](https://playwright.dev/docs/api/class-apirequest#api-request-new-context).
|
||||
* Such `APIRequestContext` object will have its own isolated cookie storage.
|
||||
*
|
||||
*/
|
||||
export interface APIRequestContext {
|
||||
/**
|
||||
|
|
@ -14188,7 +14205,6 @@ export interface APIRequestContext {
|
|||
* [APIResponse] class represents responses returned by
|
||||
* [apiRequestContext.get(url[, options])](https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-get)
|
||||
* and similar methods.
|
||||
*
|
||||
*/
|
||||
export interface APIResponse {
|
||||
/**
|
||||
|
|
@ -15633,15 +15649,14 @@ export interface FrameLocator {
|
|||
*/
|
||||
getByRole(role: "alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"|"caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"|"deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"|"group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"|"menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"|"presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|"searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"|"tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem", options?: {
|
||||
/**
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls. Available values for
|
||||
* checked are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-checked` or native `<input type=checkbox>` controls.
|
||||
*
|
||||
* Learn more about [`aria-checked`](https://www.w3.org/TR/wai-aria-1.2/#aria-checked).
|
||||
*/
|
||||
checked?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
* An attribute that is usually set by `aria-disabled` or `disabled`.
|
||||
*
|
||||
* > NOTE: Unlike most other attributes, `disabled` is inherited through the DOM hierarchy. Learn more about
|
||||
* [`aria-disabled`](https://www.w3.org/TR/wai-aria-1.2/#aria-disabled).
|
||||
|
|
@ -15649,14 +15664,20 @@ export interface FrameLocator {
|
|||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-expanded`.
|
||||
* Whether `name` is matched exactly: case-sensitive and whole-string. Defaults to false. Ignored when `name` is a regular
|
||||
* expression. Note that exact match still trims whitespace.
|
||||
*/
|
||||
exact?: boolean;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-expanded`.
|
||||
*
|
||||
* Learn more about [`aria-expanded`](https://www.w3.org/TR/wai-aria-1.2/#aria-expanded).
|
||||
*/
|
||||
expanded?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* Option that controls whether hidden elements are matched. By default, only non-hidden elements, as
|
||||
* [defined by ARIA](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), are matched by role selector.
|
||||
*
|
||||
* Learn more about [`aria-hidden`](https://www.w3.org/TR/wai-aria-1.2/#aria-hidden).
|
||||
|
|
@ -15672,21 +15693,22 @@ export interface FrameLocator {
|
|||
level?: number;
|
||||
|
||||
/**
|
||||
* A string attribute that matches [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
* Option to match the [accessible name](https://w3c.github.io/accname/#dfn-accessible-name). By default, matching is
|
||||
* case-insensitive and searches for a substring, use `exact` to control this behavior.
|
||||
*
|
||||
* Learn more about [accessible name](https://w3c.github.io/accname/#dfn-accessible-name).
|
||||
*/
|
||||
name?: string|RegExp;
|
||||
|
||||
/**
|
||||
* An attribute that is usually set by `aria-pressed`. Available values for pressed are `true`, `false` and `"mixed"`.
|
||||
* An attribute that is usually set by `aria-pressed`.
|
||||
*
|
||||
* Learn more about [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed).
|
||||
*/
|
||||
pressed?: boolean;
|
||||
|
||||
/**
|
||||
* A boolean attribute that is usually set by `aria-selected`.
|
||||
* An attribute that is usually set by `aria-selected`.
|
||||
*
|
||||
* Learn more about [`aria-selected`](https://www.w3.org/TR/wai-aria-1.2/#aria-selected).
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-react",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"description": "Playwright Component Testing for React",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"@playwright/test": "1.28.0-next",
|
||||
"@playwright/test": "1.28.1",
|
||||
"vite": "^3.2.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-solid",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"description": "Playwright Component Testing for Solid",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
"dependencies": {
|
||||
"vite": "^3.2.1",
|
||||
"vite-plugin-solid": "^2.3.10",
|
||||
"@playwright/test": "1.28.0-next"
|
||||
"@playwright/test": "1.28.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"solid-js": "^1.4.7"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-svelte",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"description": "Playwright Component Testing for Svelte",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.28.0-next",
|
||||
"@playwright/test": "1.28.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^1.1.0",
|
||||
"vite": "^3.2.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-vue",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"description": "Playwright Component Testing for Vue",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"@playwright/test": "1.28.0-next",
|
||||
"@playwright/test": "1.28.1",
|
||||
"vite": "^3.2.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/experimental-ct-vue2",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"description": "Playwright Component Testing for Vue2",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.28.0-next",
|
||||
"@playwright/test": "1.28.1",
|
||||
"@vitejs/plugin-vue2": "^2.0.0",
|
||||
"vite": "^3.2.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-firefox",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"description": "A high-level API to automate Firefox",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,6 +28,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/test",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -34,6 +34,6 @@
|
|||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
|
|||
};
|
||||
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
||||
_browserOptions: LaunchOptions;
|
||||
_storageStateName: string | undefined;
|
||||
_artifactsDir: () => string;
|
||||
_snapshotSuffix: string;
|
||||
};
|
||||
|
|
@ -151,7 +152,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
|||
permissions: [({ contextOptions }, use) => use(contextOptions.permissions), { option: true }],
|
||||
proxy: [({ contextOptions }, use) => use(contextOptions.proxy), { option: true }],
|
||||
storageState: [({ contextOptions }, use) => use(contextOptions.storageState), { option: true }],
|
||||
storageStateName: [undefined, { option: true }],
|
||||
_storageStateName: [undefined, { option: true, scope: 'worker' }],
|
||||
timezoneId: [({ contextOptions }, use) => use(contextOptions.timezoneId), { option: true }],
|
||||
userAgent: [({ contextOptions }, use) => use(contextOptions.userAgent), { option: true }],
|
||||
viewport: [({ contextOptions }, use) => use(contextOptions.viewport === undefined ? { width: 1280, height: 720 } : contextOptions.viewport), { option: true }],
|
||||
|
|
@ -181,7 +182,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
|||
permissions,
|
||||
proxy,
|
||||
storageState,
|
||||
storageStateName,
|
||||
_storageStateName,
|
||||
viewport,
|
||||
timezoneId,
|
||||
userAgent,
|
||||
|
|
@ -220,10 +221,10 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
|||
options.permissions = permissions;
|
||||
if (proxy !== undefined)
|
||||
options.proxy = proxy;
|
||||
if (storageStateName !== undefined) {
|
||||
const value = await test.info().storage().get(storageStateName);
|
||||
if (_storageStateName !== undefined) {
|
||||
const value = await (test.info() as TestInfoImpl)._storage().get(_storageStateName);
|
||||
if (!value)
|
||||
throw new Error(`Cannot find value in the storage for storageStateName: "${storageStateName}"`);
|
||||
throw new Error(`Cannot find value in the storage for storageStateName: "${_storageStateName}"`);
|
||||
options.storageState = value as any;
|
||||
} else if (storageState !== undefined) {
|
||||
options.storageState = storageState;
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ export class Loader {
|
|||
const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results'));
|
||||
const snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir);
|
||||
const name = takeFirst(projectConfig.name, config.name, '');
|
||||
const _setup = takeFirst(projectConfig.setup, []);
|
||||
const _setup = takeFirst((projectConfig as any)._setup, []);
|
||||
|
||||
const defaultSnapshotPathTemplate = '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}';
|
||||
const snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate);
|
||||
|
|
@ -614,7 +614,7 @@ function validateProject(file: string, project: Project, title: string) {
|
|||
throw errorWithFile(file, `${title}.testDir must be a string`);
|
||||
}
|
||||
|
||||
for (const prop of ['testIgnore', 'testMatch', 'setup'] as const) {
|
||||
for (const prop of ['testIgnore', 'testMatch'] as const) {
|
||||
if (prop in project && project[prop] !== undefined) {
|
||||
const value = project[prop];
|
||||
if (Array.isArray(value)) {
|
||||
|
|
|
|||
|
|
@ -17,11 +17,10 @@ import http from 'http';
|
|||
import https from 'https';
|
||||
import path from 'path';
|
||||
import net from 'net';
|
||||
import { spawn, spawnSync } from 'child_process';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
|
||||
import { debug } from 'playwright-core/lib/utilsBundle';
|
||||
import { raceAgainstTimeout } from 'playwright-core/lib/utils/timeoutRunner';
|
||||
import { launchProcess } from 'playwright-core/lib/utils/processLauncher';
|
||||
|
||||
import type { FullConfig, Reporter, Suite } from '../../types/testReporter';
|
||||
import type { TestRunnerPlugin } from '.';
|
||||
|
|
@ -47,8 +46,8 @@ const debugWebServer = debug('pw:webserver');
|
|||
|
||||
export class WebServerPlugin implements TestRunnerPlugin {
|
||||
private _isAvailable?: () => Promise<boolean>;
|
||||
private _processExitedPromise?: Promise<{ code: number|null, signal: NodeJS.Signals|null }>;
|
||||
private _childProcess?: ChildProcess;
|
||||
private _killProcess?: () => Promise<void>;
|
||||
private _processExitedPromise!: Promise<any>;
|
||||
private _options: WebServerPluginOptions;
|
||||
private _checkPortOnly: boolean;
|
||||
private _reporter?: Reporter;
|
||||
|
|
@ -65,6 +64,7 @@ export class WebServerPlugin implements TestRunnerPlugin {
|
|||
this._options.cwd = this._options.cwd ? path.resolve(configDir, this._options.cwd) : configDir;
|
||||
try {
|
||||
await this._startProcess();
|
||||
await this._waitForProcess();
|
||||
} catch (error) {
|
||||
await this.teardown();
|
||||
throw error;
|
||||
|
|
@ -72,27 +72,13 @@ export class WebServerPlugin implements TestRunnerPlugin {
|
|||
}
|
||||
|
||||
public async teardown() {
|
||||
if (!this._childProcess || !this._childProcess.pid || this._childProcess.killed)
|
||||
return;
|
||||
// Send SIGTERM and wait for it to gracefully close.
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
const taskkillProcess = spawnSync(`taskkill /pid ${this._childProcess.pid} /T /F`, { shell: true });
|
||||
const [stdout, stderr] = [taskkillProcess.stdout.toString(), taskkillProcess.stderr.toString()];
|
||||
if (stdout)
|
||||
debugWebServer(`[pid=${this._childProcess.pid}] taskkill stdout: ${stdout}`);
|
||||
if (stderr)
|
||||
debugWebServer(`[pid=${this._childProcess.pid}] taskkill stderr: ${stderr}`);
|
||||
} else {
|
||||
process.kill(this._childProcess.pid, 'SIGTERM');
|
||||
}
|
||||
} catch (e) {
|
||||
// the process might have already stopped
|
||||
}
|
||||
await this._processExitedPromise;
|
||||
await this._killProcess?.();
|
||||
}
|
||||
|
||||
private async _startProcess(): Promise<void> {
|
||||
let processExitedReject = (error: Error) => { };
|
||||
this._processExitedPromise = new Promise((_, reject) => processExitedReject = reject);
|
||||
|
||||
const isAlreadyAvailable = await this._isAvailable!();
|
||||
if (isAlreadyAvailable) {
|
||||
debugWebServer(`WebServer is already available`);
|
||||
|
|
@ -103,28 +89,33 @@ export class WebServerPlugin implements TestRunnerPlugin {
|
|||
}
|
||||
|
||||
debugWebServer(`Starting WebServer process ${this._options.command}...`);
|
||||
|
||||
this._childProcess = spawn(this._options.command, [], {
|
||||
const { launchedProcess, kill } = await launchProcess({
|
||||
command: this._options.command,
|
||||
env: {
|
||||
...DEFAULT_ENVIRONMENT_VARIABLES,
|
||||
...envWithoutExperimentalLoaderOptions(),
|
||||
...this._options.env,
|
||||
},
|
||||
cwd: this._options.cwd,
|
||||
stdio: 'pipe',
|
||||
stdio: 'stdin',
|
||||
shell: true,
|
||||
attemptToGracefullyClose: async () => {},
|
||||
log: () => {},
|
||||
onExit: code => processExitedReject(new Error(code ? `Process from config.webServer was not able to start. Exit code: ${code}` : 'Process from config.webServer exited early.')),
|
||||
tempDirectories: [],
|
||||
});
|
||||
this._processExitedPromise = new Promise((resolve, reject) => {
|
||||
this._childProcess!.once('exit', (code, signal) => resolve({ code, signal }));
|
||||
});
|
||||
this._killProcess = kill;
|
||||
|
||||
debugWebServer(`Process started`);
|
||||
|
||||
this._childProcess.stderr!.on('data', line => this._reporter!.onStdErr?.('[WebServer] ' + line.toString()));
|
||||
this._childProcess.stdout!.on('data', line => {
|
||||
launchedProcess.stderr!.on('data', line => this._reporter!.onStdErr?.('[WebServer] ' + line.toString()));
|
||||
launchedProcess.stdout!.on('data', line => {
|
||||
if (debugWebServer.enabled)
|
||||
this._reporter!.onStdOut?.('[WebServer] ' + line.toString());
|
||||
});
|
||||
}
|
||||
|
||||
private async _waitForProcess() {
|
||||
debugWebServer(`Waiting for availability...`);
|
||||
await this._waitForAvailability();
|
||||
debugWebServer(`WebServer available`);
|
||||
|
|
@ -135,9 +126,7 @@ export class WebServerPlugin implements TestRunnerPlugin {
|
|||
const cancellationToken = { canceled: false };
|
||||
const { timedOut } = (await Promise.race([
|
||||
raceAgainstTimeout(() => waitFor(this._isAvailable!, cancellationToken), launchTimeout),
|
||||
this._processExitedPromise!.then(({ code, signal }) => {
|
||||
throw new Error(`Process from config.webServer terminated with exit code "${code}" and signal "${signal}"`);
|
||||
}),
|
||||
this._processExitedPromise,
|
||||
]));
|
||||
cancellationToken.canceled = true;
|
||||
if (timedOut)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { monotonicTime } from 'playwright-core/lib/utils';
|
||||
import type { Storage, TestError, TestInfo, TestStatus } from '../types/test';
|
||||
import type { TestError, TestInfo, TestStatus } from '../types/test';
|
||||
import type { WorkerInitParams } from './ipc';
|
||||
import type { Loader } from './loader';
|
||||
import type { TestCase } from './test';
|
||||
|
|
@ -60,7 +60,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
readonly snapshotDir: string;
|
||||
errors: TestError[] = [];
|
||||
currentStep: TestStepInternal | undefined;
|
||||
private readonly _storage: JsonStorage;
|
||||
private readonly _testStorage: JsonStorage;
|
||||
|
||||
get error(): TestError | undefined {
|
||||
return this.errors[0];
|
||||
|
|
@ -108,7 +108,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
this.expectedStatus = test.expectedStatus;
|
||||
|
||||
this._timeoutManager = new TimeoutManager(this.project.timeout);
|
||||
this._storage = new JsonStorage(this);
|
||||
this._testStorage = new JsonStorage(this);
|
||||
|
||||
this.outputDir = (() => {
|
||||
const relativeTestFilePath = path.relative(this.project.testDir, test._requireFile.replace(/\.(spec|test)\.(js|ts|mjs)$/, ''));
|
||||
|
|
@ -281,12 +281,12 @@ export class TestInfoImpl implements TestInfo {
|
|||
this._timeoutManager.setTimeout(timeout);
|
||||
}
|
||||
|
||||
storage() {
|
||||
return this._storage;
|
||||
_storage() {
|
||||
return this._testStorage;
|
||||
}
|
||||
}
|
||||
|
||||
class JsonStorage implements Storage {
|
||||
class JsonStorage {
|
||||
constructor(private _testInfo: TestInfoImpl) {
|
||||
}
|
||||
|
||||
|
|
|
|||
38
packages/playwright-test/types/test.d.ts
vendored
38
packages/playwright-test/types/test.d.ts
vendored
|
|
@ -1849,11 +1849,6 @@ export interface TestInfo {
|
|||
*/
|
||||
stdout: Array<string|Buffer>;
|
||||
|
||||
/**
|
||||
* Returns a [Storage] instance for the currently running project.
|
||||
*/
|
||||
storage(): Storage;
|
||||
|
||||
/**
|
||||
* Timeout in milliseconds for the currently running test. Zero means no timeout. Learn more about
|
||||
* [various timeouts](https://playwright.dev/docs/test-timeouts).
|
||||
|
|
@ -2782,24 +2777,6 @@ type ConnectOptions = {
|
|||
timeout?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Playwright Test provides a [testInfo.storage()](https://playwright.dev/docs/api/class-testinfo#test-info-storage) object
|
||||
* for passing values between project setup and tests. TODO: examples
|
||||
*/
|
||||
export interface Storage {
|
||||
/**
|
||||
* Get named item from the storage. Returns undefined if there is no value with given name.
|
||||
* @param name Item name.
|
||||
*/
|
||||
get<T>(name: string): Promise<T | undefined>;
|
||||
/**
|
||||
* Set value to the storage.
|
||||
* @param name Item name.
|
||||
* @param value Item value. The value must be serializable to JSON. Passing `undefined` deletes the entry with given name.
|
||||
*/
|
||||
set<T>(name: string, value: T | undefined): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Playwright Test provides many options to configure test environment, [Browser], [BrowserContext] and more.
|
||||
*
|
||||
|
|
@ -3034,15 +3011,6 @@ export interface PlaywrightTestOptions {
|
|||
* Either a path to the file with saved storage, or an object with the following fields:
|
||||
*/
|
||||
storageState: StorageState | undefined;
|
||||
/**
|
||||
* Name of the [Storage] entry that should be used to initialize
|
||||
* [testOptions.storageState](https://playwright.dev/docs/api/class-testoptions#test-options-storage-state). The value must
|
||||
* be written to the storage before creatiion of a browser context that uses it (usually in
|
||||
* [testProject.setup](https://playwright.dev/docs/api/class-testproject#test-project-setup)). If both this property and
|
||||
* [testOptions.storageState](https://playwright.dev/docs/api/class-testoptions#test-options-storage-state) are specified,
|
||||
* this property will always take precedence.
|
||||
*/
|
||||
storageStateName: string | undefined;
|
||||
/**
|
||||
* Changes the timezone of the context. See
|
||||
* [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1)
|
||||
|
|
@ -4555,12 +4523,6 @@ interface TestProject {
|
|||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Project setup files that would be executed before all tests in the project. If project setup fails the tests in this
|
||||
* project will be skipped. All project setup files will run in every shard if the project is sharded.
|
||||
*/
|
||||
setup?: string|RegExp|Array<string|RegExp>;
|
||||
|
||||
/**
|
||||
* The base directory, relative to the config file, for snapshot files created with `toMatchSnapshot`. Defaults to
|
||||
* [testProject.testDir](https://playwright.dev/docs/api/class-testproject#test-project-test-dir).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-webkit",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"description": "A high-level API to automate WebKit",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,6 +28,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright",
|
||||
"version": "1.28.0-next",
|
||||
"version": "1.28.1",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -28,6 +28,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.28.0-next"
|
||||
"playwright-core": "1.28.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,11 @@
|
|||
}
|
||||
|
||||
.recorder .toolbar-button.toggled.record {
|
||||
color: #fd1e1e;
|
||||
color: #a1260d;
|
||||
}
|
||||
|
||||
body.dark-mode .recorder .toolbar-button.toggled.record {
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.recorder .toolbar-button:not([disabled]) .codicon-debug-continue,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,11 @@
|
|||
color: #0070c1;
|
||||
}
|
||||
|
||||
.CodeMirror span.cm-property, .CodeMirror span.cm-qualifier, .CodeMirror span.cm-attribute {
|
||||
.CodeMirror span.cm-property {
|
||||
color: #795e26;
|
||||
}
|
||||
|
||||
.CodeMirror span.cm-qualifier, .CodeMirror span.cm-attribute {
|
||||
color: #001080;
|
||||
}
|
||||
|
||||
|
|
@ -70,31 +74,36 @@
|
|||
color: #267f99;
|
||||
}
|
||||
|
||||
@media(prefers-color-scheme: dark) {
|
||||
|
||||
.CodeMirror span.cm-def, .CodeMirror span.cm-tag {
|
||||
body.dark-mode .CodeMirror span.cm-def,
|
||||
body.dark-mode .CodeMirror span.cm-tag {
|
||||
color: var(--vscode-debugView-valueChangedHighlight);
|
||||
}
|
||||
|
||||
.CodeMirror span.cm-comment, .CodeMirror span.cm-link {
|
||||
body.dark-mode .CodeMirror span.cm-comment,
|
||||
body.dark-mode .CodeMirror span.cm-link {
|
||||
color: #6a9955;
|
||||
}
|
||||
|
||||
.CodeMirror span.cm-variable, .CodeMirror span.cm-variable-2, .CodeMirror span.cm-atom {
|
||||
body.dark-mode .CodeMirror span.cm-variable,
|
||||
body.dark-mode .CodeMirror span.cm-variable-2,
|
||||
body.dark-mode .CodeMirror span.cm-atom {
|
||||
color: #4fc1ff;
|
||||
}
|
||||
|
||||
.CodeMirror span.cm-property, .CodeMirror span.cm-qualifier, .CodeMirror span.cm-attribute {
|
||||
body.dark-mode .CodeMirror span.cm-property {
|
||||
color: #dcdcaa;
|
||||
}
|
||||
|
||||
body.dark-mode .CodeMirror span.cm-qualifier,
|
||||
body.dark-mode .CodeMirror span.cm-attribute {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.CodeMirror span.cm-variable-3,
|
||||
.CodeMirror span.cm-type {
|
||||
body.dark-mode .CodeMirror span.cm-variable-3,
|
||||
body.dark-mode .CodeMirror span.cm-type {
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.CodeMirror span.cm-bracket {
|
||||
color: var(--vscode-editorBracketHighlight-foreground3);
|
||||
}
|
||||
|
|
|
|||
12
tests/electron/electron-app-ready-event.js
Normal file
12
tests/electron/electron-app-ready-event.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
const { app } = require('electron');
|
||||
|
||||
globalThis.__playwrightLog = [];
|
||||
|
||||
globalThis.__playwrightLog.push(`isReady == ${app.isReady()}`);
|
||||
app.whenReady().then(() => {
|
||||
globalThis.__playwrightLog.push(`whenReady resolved`);
|
||||
globalThis.__playwrightLog.push(`isReady == ${app.isReady()}`);
|
||||
});
|
||||
|
||||
app.on('will-finish-launching', () => globalThis.__playwrightLog.push('will-finish-launching fired'));
|
||||
app.on('ready', () => globalThis.__playwrightLog.push('ready fired'));
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
const { app, protocol } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
app.commandLine.appendSwitch('disable-features', 'AutoExpandDetailsElement');
|
||||
app.commandLine.appendSwitch('allow-pre-commit-input')
|
||||
|
||||
app.on('window-all-closed', e => e.preventDefault());
|
||||
|
||||
app.whenReady().then(() => {
|
||||
|
|
|
|||
|
|
@ -33,11 +33,34 @@ test('should fire close event', async ({ playwright }) => {
|
|||
expect(events.join('|')).toBe('context|application');
|
||||
});
|
||||
|
||||
test('should dispatch ready event', async ({ playwright }) => {
|
||||
const electronApp = await playwright._electron.launch({
|
||||
args: [path.join(__dirname, 'electron-app-ready-event.js')],
|
||||
});
|
||||
try {
|
||||
const events = await electronApp.evaluate(() => globalThis.__playwrightLog);
|
||||
expect(events).toEqual([
|
||||
'isReady == false',
|
||||
'will-finish-launching fired',
|
||||
'ready fired',
|
||||
'whenReady resolved',
|
||||
'isReady == true',
|
||||
]);
|
||||
} finally {
|
||||
await electronApp.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('should script application', async ({ electronApp }) => {
|
||||
const appPath = await electronApp.evaluate(async ({ app }) => app.getAppPath());
|
||||
expect(appPath).toBe(path.resolve(__dirname));
|
||||
});
|
||||
|
||||
test('should preserve args', async ({ electronApp }) => {
|
||||
const argv = await electronApp.evaluate(async ({ app }) => process.argv);
|
||||
expect(argv.slice(1)).toEqual([expect.stringContaining('electron/electron-app.js')]);
|
||||
});
|
||||
|
||||
test('should return windows', async ({ electronApp, newWindow }) => {
|
||||
const window = await newWindow();
|
||||
expect(electronApp.windows()).toEqual([window]);
|
||||
|
|
@ -131,7 +154,7 @@ test('should create page for browser view', async ({ playwright }) => {
|
|||
const app = await playwright._electron.launch({
|
||||
args: [path.join(__dirname, 'electron-window-app.js')],
|
||||
});
|
||||
const browserViewPagePromise = app.waitForEvent('window');
|
||||
await app.firstWindow();
|
||||
await app.evaluate(async electron => {
|
||||
const window = electron.BrowserWindow.getAllWindows()[0];
|
||||
const view = new electron.BrowserView();
|
||||
|
|
@ -139,8 +162,7 @@ test('should create page for browser view', async ({ playwright }) => {
|
|||
await view.webContents.loadURL('about:blank');
|
||||
view.setBounds({ x: 0, y: 0, width: 256, height: 256 });
|
||||
});
|
||||
await browserViewPagePromise;
|
||||
expect(app.windows()).toHaveLength(2);
|
||||
await expect.poll(() => app.windows().length).toBe(2);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
|
|
@ -148,7 +170,7 @@ test('should return same browser window for browser view pages', async ({ playwr
|
|||
const app = await playwright._electron.launch({
|
||||
args: [path.join(__dirname, 'electron-window-app.js')],
|
||||
});
|
||||
const browserViewPagePromise = app.waitForEvent('window');
|
||||
await app.firstWindow();
|
||||
await app.evaluate(async electron => {
|
||||
const window = electron.BrowserWindow.getAllWindows()[0];
|
||||
const view = new electron.BrowserView();
|
||||
|
|
@ -156,7 +178,7 @@ test('should return same browser window for browser view pages', async ({ playwr
|
|||
await view.webContents.loadURL('about:blank');
|
||||
view.setBounds({ x: 0, y: 0, width: 256, height: 256 });
|
||||
});
|
||||
await browserViewPagePromise;
|
||||
await expect.poll(() => app.windows().length).toBe(2);
|
||||
const [firstWindowId, secondWindowId] = await Promise.all(
|
||||
app.windows().map(async page => {
|
||||
const bwHandle = await app.browserWindow(page);
|
||||
|
|
@ -183,7 +205,6 @@ test('should record video', async ({ playwright }, testInfo) => {
|
|||
|
||||
test('should be able to get the first window when with a delayed navigation', async ({ playwright }) => {
|
||||
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/17765' });
|
||||
test.fixme();
|
||||
|
||||
const app = await playwright._electron.launch({
|
||||
args: [path.join(__dirname, 'electron-window-app-delayed-loadURL.js')],
|
||||
|
|
|
|||
|
|
@ -71,10 +71,10 @@ test('should pick element', async ({ backend, connectedBrowser }) => {
|
|||
|
||||
expect(events).toEqual([
|
||||
{
|
||||
selector: 'internal:role=button[name=\"Submit\"s]',
|
||||
selector: 'internal:role=button[name=\"Submit\"i]',
|
||||
locator: 'getByRole(\'button\', { name: \'Submit\' })',
|
||||
}, {
|
||||
selector: 'internal:role=button[name=\"Submit\"s]',
|
||||
selector: 'internal:role=button[name=\"Submit\"i]',
|
||||
locator: 'getByRole(\'button\', { name: \'Submit\' })',
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ test.describe('cli codegen', () => {
|
|||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
page.dispatchEvent('button', 'click', { detail: 1 })
|
||||
recorder.trustedClick(),
|
||||
]);
|
||||
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`
|
||||
|
|
@ -52,6 +52,27 @@ test.describe('cli codegen', () => {
|
|||
expect(message.text()).toBe('click');
|
||||
});
|
||||
|
||||
|
||||
test('should ignore programmatic events', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`<button onclick="console.log('click')">Submit</button>`);
|
||||
|
||||
const locator = await recorder.hoverOverElement('button');
|
||||
expect(locator).toBe(`getByRole('button', { name: 'Submit' })`);
|
||||
|
||||
await page.dispatchEvent('button', 'click', { detail: 1 });
|
||||
|
||||
await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
recorder.trustedClick()
|
||||
]);
|
||||
|
||||
const clicks = recorder.sources().get('Playwright Test').actions.filter(l => l.includes('Submit'));
|
||||
expect(clicks.length).toBe(1);
|
||||
});
|
||||
|
||||
test('should click after same-document navigation', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
|
||||
|
|
@ -74,7 +95,7 @@ test.describe('cli codegen', () => {
|
|||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
page.dispatchEvent('button', 'click', { detail: 1 })
|
||||
recorder.trustedClick(),
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
|
|
@ -95,16 +116,14 @@ test.describe('cli codegen', () => {
|
|||
</script>
|
||||
`);
|
||||
|
||||
const locator = await recorder.waitForHighlight(() => recorder.page.hover('canvas', {
|
||||
const locator = await recorder.hoverOverElement('canvas', {
|
||||
position: { x: 250, y: 250 },
|
||||
}));
|
||||
});
|
||||
expect(locator).toBe(`locator('canvas')`);
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
recorder.page.click('canvas', {
|
||||
position: { x: 250, y: 250 },
|
||||
})
|
||||
recorder.trustedClick(),
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
|
|
@ -154,7 +173,7 @@ test.describe('cli codegen', () => {
|
|||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
page.dispatchEvent('button', 'click', { detail: 1 })
|
||||
recorder.trustedClick(),
|
||||
]);
|
||||
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`
|
||||
|
|
@ -182,13 +201,12 @@ test.describe('cli codegen', () => {
|
|||
|
||||
// Force highlight.
|
||||
await recorder.hoverOverElement('span');
|
||||
|
||||
// Append text after highlight.
|
||||
await page.evaluate(() => {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('onclick', "console.log('click')");
|
||||
div.textContent = ' Some long text here ';
|
||||
document.documentElement.appendChild(div);
|
||||
document.body.appendChild(div);
|
||||
});
|
||||
|
||||
const locator = await recorder.hoverOverElement('div');
|
||||
|
|
@ -200,7 +218,7 @@ test.describe('cli codegen', () => {
|
|||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
page.dispatchEvent('div', 'click', { detail: 1 })
|
||||
recorder.trustedMove('div').then(() => recorder.trustedClick()),
|
||||
]);
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
await page.getByText('Some long text here').click();`);
|
||||
|
|
@ -585,7 +603,7 @@ test.describe('cli codegen', () => {
|
|||
const [popup, sources] = await Promise.all([
|
||||
page.context().waitForEvent('page'),
|
||||
recorder.waitForOutput('JavaScript', 'waitForEvent'),
|
||||
page.dispatchEvent('a', 'click', { detail: 1 })
|
||||
recorder.trustedClick(),
|
||||
]);
|
||||
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`
|
||||
|
|
@ -628,7 +646,7 @@ test.describe('cli codegen', () => {
|
|||
const [, sources] = await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
recorder.waitForOutput('JavaScript', '.click()'),
|
||||
page.dispatchEvent('a', 'click', { detail: 1 })
|
||||
recorder.trustedClick(),
|
||||
]);
|
||||
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ test.describe('cli codegen', () => {
|
|||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
page.dispatchEvent('button', 'click', { detail: 1 })
|
||||
recorder.trustedClick()
|
||||
]);
|
||||
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`
|
||||
|
|
@ -68,7 +68,7 @@ test.describe('cli codegen', () => {
|
|||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
page.dispatchEvent('button', 'click', { detail: 1 })
|
||||
recorder.trustedClick()
|
||||
]);
|
||||
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`
|
||||
|
|
@ -240,7 +240,7 @@ test.describe('cli codegen', () => {
|
|||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
page.dispatchEvent('div', 'click', { detail: 1 })
|
||||
recorder.trustedClick(),
|
||||
]);
|
||||
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`
|
||||
|
|
@ -271,7 +271,7 @@ test.describe('cli codegen', () => {
|
|||
|
||||
const [sources] = await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
page.dispatchEvent('input', 'click', { detail: 1 })
|
||||
recorder.trustedClick(),
|
||||
]);
|
||||
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`
|
||||
|
|
@ -300,7 +300,7 @@ test.describe('cli codegen', () => {
|
|||
|
||||
const [sources] = await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
page.dispatchEvent('input', 'click', { detail: 1 })
|
||||
recorder.trustedClick(),
|
||||
]);
|
||||
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`
|
||||
|
|
@ -329,7 +329,7 @@ test.describe('cli codegen', () => {
|
|||
|
||||
const [sources] = await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
page.dispatchEvent('input', 'click', { detail: 1 })
|
||||
recorder.trustedClick(),
|
||||
]);
|
||||
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`
|
||||
|
|
@ -358,7 +358,7 @@ test.describe('cli codegen', () => {
|
|||
|
||||
const [sources] = await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
page.dispatchEvent('input', 'click', { detail: 1 })
|
||||
recorder.trustedClick(),
|
||||
]);
|
||||
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`
|
||||
|
|
|
|||
|
|
@ -171,8 +171,22 @@ class Recorder {
|
|||
return new Promise(f => callback = f);
|
||||
}
|
||||
|
||||
async hoverOverElement(selector: string): Promise<string> {
|
||||
return this.waitForHighlight(() => this.page.dispatchEvent(selector, 'mousemove', { detail: 1 }));
|
||||
async hoverOverElement(selector: string, options?: { position?: { x: number, y: number }}): Promise<string> {
|
||||
return this.waitForHighlight(async () => {
|
||||
const box = await this.page.locator(selector).first().boundingBox();
|
||||
const offset = options?.position || { x: box.width / 2, y: box.height / 2 };
|
||||
await this.page.mouse.move(box.x + offset.x, box.y + offset.y);
|
||||
});
|
||||
}
|
||||
|
||||
async trustedMove(selector: string) {
|
||||
const box = await this.page.locator(selector).first().boundingBox();
|
||||
await this.page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
}
|
||||
|
||||
async trustedClick() {
|
||||
await this.page.mouse.down();
|
||||
await this.page.mouse.up();
|
||||
}
|
||||
|
||||
async focusElement(selector: string): Promise<string> {
|
||||
|
|
|
|||
|
|
@ -175,6 +175,39 @@ it('reverse engineer locators', async ({ page }) => {
|
|||
});
|
||||
});
|
||||
|
||||
it('reverse engineer getByRole', async ({ page }) => {
|
||||
expect.soft(generate(page.getByRole('button'))).toEqual({
|
||||
javascript: `getByRole('button')`,
|
||||
python: `get_by_role("button")`,
|
||||
java: `getByRole(AriaRole.BUTTON)`,
|
||||
csharp: `GetByRole(AriaRole.Button)`,
|
||||
});
|
||||
expect.soft(generate(page.getByRole('button', { name: 'Hello' }))).toEqual({
|
||||
javascript: `getByRole('button', { name: 'Hello' })`,
|
||||
python: `get_by_role("button", name="Hello")`,
|
||||
java: `getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Hello"))`,
|
||||
csharp: `GetByRole(AriaRole.Button, new() { NameString = "Hello" })`,
|
||||
});
|
||||
expect.soft(generate(page.getByRole('button', { name: /Hello/ }))).toEqual({
|
||||
javascript: `getByRole('button', { name: /Hello/ })`,
|
||||
python: `get_by_role("button", name=re.compile(r"Hello"))`,
|
||||
java: `getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(Pattern.compile("Hello")))`,
|
||||
csharp: `GetByRole(AriaRole.Button, new() { NameRegex = new Regex("Hello") })`,
|
||||
});
|
||||
expect.soft(generate(page.getByRole('button', { name: 'He"llo', exact: true }))).toEqual({
|
||||
javascript: `getByRole('button', { name: 'He"llo', exact: true })`,
|
||||
python: `get_by_role("button", name="He\\"llo", exact=True)`,
|
||||
java: `getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("He\\"llo").setExact(true))`,
|
||||
csharp: `GetByRole(AriaRole.Button, new() { NameString = "He\\"llo", Exact = true })`,
|
||||
});
|
||||
expect.soft(generate(page.getByRole('button', { checked: true, pressed: false, level: 3 }))).toEqual({
|
||||
javascript: `getByRole('button', { checked: true, level: 3, pressed: false })`,
|
||||
python: `get_by_role("button", checked=True, level=3, pressed=False)`,
|
||||
java: `getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setChecked(true).setLevel(3).setPressed(false))`,
|
||||
csharp: `GetByRole(AriaRole.Button, new() { Checked = true, Level = 3, Pressed = false })`,
|
||||
});
|
||||
});
|
||||
|
||||
it('reverse engineer ignore-case locators', async ({ page }) => {
|
||||
expect.soft(generate(page.getByText('hello my\nwo"rld'))).toEqual({
|
||||
csharp: 'GetByText("hello my\\nwo\\"rld")',
|
||||
|
|
@ -244,14 +277,14 @@ it('reverse engineer hasText', async ({ page }) => {
|
|||
});
|
||||
|
||||
expect.soft(generate(page.getByText('Hello').filter({ hasText: /wo\/\srld\n/ }))).toEqual({
|
||||
csharp: `GetByText("Hello").Filter(new() { HasTextString = new Regex("wo\\\\/\\\\srld\\\\n") })`,
|
||||
csharp: `GetByText("Hello").Filter(new() { HasTextRegex = new Regex("wo\\\\/\\\\srld\\\\n") })`,
|
||||
java: `getByText("Hello").filter(new Locator.LocatorOptions().setHasText(Pattern.compile("wo\\\\/\\\\srld\\\\n")))`,
|
||||
javascript: `getByText('Hello').filter({ hasText: /wo\\/\\srld\\n/ })`,
|
||||
python: `get_by_text("Hello").filter(has_text=re.compile(r"wo/\\srld\\n"))`,
|
||||
});
|
||||
|
||||
expect.soft(generate(page.getByText('Hello').filter({ hasText: /wor"ld/ }))).toEqual({
|
||||
csharp: `GetByText("Hello").Filter(new() { HasTextString = new Regex("wor\\"ld") })`,
|
||||
csharp: `GetByText("Hello").Filter(new() { HasTextRegex = new Regex("wor\\"ld") })`,
|
||||
java: `getByText("Hello").filter(new Locator.LocatorOptions().setHasText(Pattern.compile("wor\\"ld")))`,
|
||||
javascript: `getByText('Hello').filter({ hasText: /wor"ld/ })`,
|
||||
python: `get_by_text("Hello").filter(has_text=re.compile(r"wor\\"ld"))`,
|
||||
|
|
@ -283,14 +316,14 @@ it('reverse engineer frameLocator', async ({ page }) => {
|
|||
const locator = page
|
||||
.frameLocator('iframe')
|
||||
.getByText('foo', { exact: true })
|
||||
.frameLocator('frame')
|
||||
.frameLocator('frame').first()
|
||||
.frameLocator('iframe')
|
||||
.locator('span');
|
||||
expect.soft(generate(locator)).toEqual({
|
||||
csharp: `FrameLocator("iframe").GetByText("foo", new() { Exact = true }).FrameLocator("frame").FrameLocator("iframe").Locator("span")`,
|
||||
java: `frameLocator("iframe").getByText("foo", new FrameLocator.GetByTextOptions().setExact(true)).frameLocator("frame").frameLocator("iframe").locator("span")`,
|
||||
javascript: `frameLocator('iframe').getByText('foo', { exact: true }).frameLocator('frame').frameLocator('iframe').locator('span')`,
|
||||
python: `frame_locator("iframe").get_by_text("foo", exact=True).frame_locator("frame").frame_locator("iframe").locator("span")`,
|
||||
csharp: `FrameLocator("iframe").GetByText("foo", new() { Exact = true }).FrameLocator("frame").First.FrameLocator("iframe").Locator("span")`,
|
||||
java: `frameLocator("iframe").getByText("foo", new FrameLocator.GetByTextOptions().setExact(true)).frameLocator("frame").first().frameLocator("iframe").locator("span")`,
|
||||
javascript: `frameLocator('iframe').getByText('foo', { exact: true }).frameLocator('frame').first().frameLocator('iframe').locator('span')`,
|
||||
python: `frame_locator("iframe").get_by_text("foo", exact=True).frame_locator("frame").first.frame_locator("iframe").locator("span")`,
|
||||
});
|
||||
|
||||
// Note that frame locators with ">>" are not restored back due to ambiguity.
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ it.describe('selector generator', () => {
|
|||
|
||||
it('should generate text for <input type=button>', async ({ page }) => {
|
||||
await page.setContent(`<input type=button value="Click me">`);
|
||||
expect(await generate(page, 'input')).toBe('internal:role=button[name=\"Click me\"s]');
|
||||
expect(await generate(page, 'input')).toBe('internal:role=button[name=\"Click me\"i]');
|
||||
});
|
||||
|
||||
it('should trim text', async ({ page }) => {
|
||||
|
|
@ -347,7 +347,7 @@ it.describe('selector generator', () => {
|
|||
|
||||
await page.setContent(`<button><span></span></button><button></button>`);
|
||||
await page.$eval('button', button => button.setAttribute('aria-label', `!#'!?:`));
|
||||
expect(await generate(page, 'button')).toBe(`internal:role=button[name="!#'!?:"s]`);
|
||||
expect(await generate(page, 'button')).toBe(`internal:role=button[name="!#'!?:"i]`);
|
||||
expect(await page.$(`role=button[name="!#'!?:"]`)).toBeTruthy();
|
||||
|
||||
await page.setContent(`<div><span></span></div>`);
|
||||
|
|
@ -371,7 +371,7 @@ it.describe('selector generator', () => {
|
|||
|
||||
it('should accept valid aria-label for candidate consideration', async ({ page }) => {
|
||||
await page.setContent(`<button aria-label="ariaLabel" id="buttonId"></button>`);
|
||||
expect(await generate(page, 'button')).toBe('internal:role=button[name=\"ariaLabel\"s]');
|
||||
expect(await generate(page, 'button')).toBe('internal:role=button[name=\"ariaLabel\"i]');
|
||||
});
|
||||
|
||||
it('should ignore empty role for candidate consideration', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -97,8 +97,8 @@ it('should work for $ and $$', async ({ page, server }) => {
|
|||
|
||||
it('should wait for frame', async ({ page, server }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const error = await page.frameLocator('iframe').locator('span').click({ timeout: 1000 }).catch(e => e);
|
||||
expect(error.message).toContain('waiting for frameLocator(\'iframe\')');
|
||||
const error = await page.locator('body').frameLocator('iframe').locator('span').click({ timeout: 1000 }).catch(e => e);
|
||||
expect(error.message).toContain(`waiting for locator('body').frameLocator('iframe')`);
|
||||
});
|
||||
|
||||
it('should wait for frame 2', async ({ page, server }) => {
|
||||
|
|
|
|||
|
|
@ -144,3 +144,36 @@ world</label><input id=control />`);
|
|||
await expect(page.getByAltText('hello my\nworld')).toHaveAttribute('id', 'control');
|
||||
await expect(page.getByTitle('hello my\nworld')).toHaveAttribute('id', 'control');
|
||||
});
|
||||
|
||||
it('getByRole escaping', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<a href="https://playwright.dev">issues 123</a>
|
||||
<a href="https://playwright.dev">he llo 56</a>
|
||||
<button>Click me</button>
|
||||
`);
|
||||
expect.soft(await page.getByRole('button').evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button>Click me</button>`,
|
||||
]);
|
||||
expect.soft(await page.getByRole('link').evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<a href="https://playwright.dev">issues 123</a>`,
|
||||
`<a href="https://playwright.dev">he llo 56</a>`,
|
||||
]);
|
||||
|
||||
expect.soft(await page.getByRole('link', { name: 'issues 123' }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<a href="https://playwright.dev">issues 123</a>`,
|
||||
]);
|
||||
expect.soft(await page.getByRole('link', { name: 'sues' }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<a href="https://playwright.dev">issues 123</a>`,
|
||||
]);
|
||||
expect.soft(await page.getByRole('link', { name: ' he \n llo ' }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<a href="https://playwright.dev">he llo 56</a>`,
|
||||
]);
|
||||
expect.soft(await page.getByRole('button', { name: 'issues' }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
]);
|
||||
|
||||
expect.soft(await page.getByRole('link', { name: 'sues', exact: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
]);
|
||||
expect.soft(await page.getByRole('link', { name: ' he \n llo 56 ', exact: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<a href="https://playwright.dev">he llo 56</a>`,
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -169,26 +169,42 @@ test('should support pressed', async ({ page }) => {
|
|||
|
||||
test('should support expanded', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<button>Hi</button>
|
||||
<button aria-expanded="true">Hello</button>
|
||||
<button aria-expanded="false">Bye</button>
|
||||
<div role="treeitem">Hi</div>
|
||||
<div role="treeitem" aria-expanded="true">Hello</div>
|
||||
<div role="treeitem" aria-expanded="false">Bye</div>
|
||||
`);
|
||||
expect(await page.locator(`role=button[expanded]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button aria-expanded="true">Hello</button>`,
|
||||
|
||||
expect(await page.locator('role=treeitem').evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="treeitem">Hi</div>`,
|
||||
`<div role="treeitem" aria-expanded="true">Hello</div>`,
|
||||
`<div role="treeitem" aria-expanded="false">Bye</div>`,
|
||||
]);
|
||||
expect(await page.locator(`role=button[expanded=true]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button aria-expanded="true">Hello</button>`,
|
||||
expect(await page.getByRole('treeitem').evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="treeitem">Hi</div>`,
|
||||
`<div role="treeitem" aria-expanded="true">Hello</div>`,
|
||||
`<div role="treeitem" aria-expanded="false">Bye</div>`,
|
||||
]);
|
||||
expect(await page.getByRole('button', { expanded: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button aria-expanded="true">Hello</button>`,
|
||||
|
||||
expect(await page.locator(`role=treeitem[expanded]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="treeitem" aria-expanded="true">Hello</div>`,
|
||||
]);
|
||||
expect(await page.locator(`role=button[expanded=false]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button>Hi</button>`,
|
||||
`<button aria-expanded="false">Bye</button>`,
|
||||
expect(await page.locator(`role=treeitem[expanded=true]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="treeitem" aria-expanded="true">Hello</div>`,
|
||||
]);
|
||||
expect(await page.getByRole('button', { expanded: false }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<button>Hi</button>`,
|
||||
`<button aria-expanded="false">Bye</button>`,
|
||||
expect(await page.getByRole('treeitem', { expanded: true }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="treeitem" aria-expanded="true">Hello</div>`,
|
||||
]);
|
||||
|
||||
expect(await page.locator(`role=treeitem[expanded=false]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="treeitem" aria-expanded="false">Bye</div>`,
|
||||
]);
|
||||
expect(await page.getByRole('treeitem', { expanded: false }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="treeitem" aria-expanded="false">Bye</div>`,
|
||||
]);
|
||||
|
||||
// Workaround for expanded="none".
|
||||
expect(await page.locator(`[role=treeitem]:not([aria-expanded])`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="treeitem">Hi</div>`,
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -326,6 +342,9 @@ test('should support name', async ({ page }) => {
|
|||
expect(await page.locator(`role=button[name="Hello"]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label=" Hello "></div>`,
|
||||
]);
|
||||
expect(await page.locator(`role=button[name=" \n Hello "]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label=" Hello "></div>`,
|
||||
]);
|
||||
expect(await page.getByRole('button', { name: 'Hello' }).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||
`<div role="button" aria-label=" Hello "></div>`,
|
||||
]);
|
||||
|
|
@ -400,4 +419,7 @@ test('errors', async ({ page }) => {
|
|||
|
||||
const e7 = await page.$('role=button[name]').catch(e => e);
|
||||
expect(e7.message).toContain(`"name" attribute must have a value`);
|
||||
|
||||
const e8 = await page.$('role=treeitem[expanded="none"]').catch(e => e);
|
||||
expect(e8.message).toContain(`"expanded" must be one of true, false`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -480,41 +480,3 @@ test('should have correct types for the config', async ({ runTSC }) => {
|
|||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should throw when project.setup has wrong type', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
module.exports = {
|
||||
projects: [
|
||||
{ name: 'a', setup: 100 },
|
||||
],
|
||||
};
|
||||
`,
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('pass', async () => {});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output).toContain(`Error: playwright.config.ts: config.projects[0].setup must be a string or a RegExp`);
|
||||
});
|
||||
|
||||
test('should throw when project.setup has wrong array type', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
module.exports = {
|
||||
projects: [
|
||||
{ name: 'a', setup: [/100/, 100] },
|
||||
],
|
||||
};
|
||||
`,
|
||||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('pass', async () => {});
|
||||
`
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.output).toContain(`Error: playwright.config.ts: config.projects[0].setup[1] must be a string or a RegExp`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -374,3 +374,27 @@ test('should reuse context with beforeunload', async ({ runInlineTest }) => {
|
|||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(2);
|
||||
});
|
||||
|
||||
test('should cancel pending operations upon reuse', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'src/reuse.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('one', async ({ page }) => {
|
||||
await Promise.race([
|
||||
page.getByText('click me').click().catch(e => {}),
|
||||
page.waitForTimeout(2000),
|
||||
]);
|
||||
});
|
||||
|
||||
test('two', async ({ page }) => {
|
||||
await page.setContent('<button onclick="window._clicked=true">click me</button>');
|
||||
// Give it time to erroneously click.
|
||||
await page.waitForTimeout(2000);
|
||||
expect(await page.evaluate('window._clicked')).toBe(undefined);
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 }, { PW_TEST_REUSE_CONTEXT: '1' });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(2);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ function expectFilesRunBefore(timeline: Timeline, before: string[], after: strin
|
|||
test('should work for one project', async ({ runGroups }, testInfo) => {
|
||||
const projectTemplates = {
|
||||
'a': {
|
||||
setup: ['**/*.setup.ts']
|
||||
_setup: ['**/*.setup.ts']
|
||||
},
|
||||
};
|
||||
const configWithFiles = createConfigWithProjects(['a'], testInfo, projectTemplates);
|
||||
|
|
@ -114,13 +114,13 @@ a > a${path.sep}a.spec.ts > a test [end]`);
|
|||
test('should work for several projects', async ({ runGroups }, testInfo) => {
|
||||
const projectTemplates = {
|
||||
'a': {
|
||||
setup: ['**/*.setup.ts']
|
||||
_setup: ['**/*.setup.ts']
|
||||
},
|
||||
'b': {
|
||||
setup: /.*b.setup.ts/
|
||||
_setup: /.*b.setup.ts/
|
||||
},
|
||||
'c': {
|
||||
setup: '**/c.setup.ts'
|
||||
_setup: '**/c.setup.ts'
|
||||
},
|
||||
};
|
||||
const configWithFiles = createConfigWithProjects(['a', 'b', 'c'], testInfo, projectTemplates);
|
||||
|
|
@ -134,10 +134,10 @@ test('should work for several projects', async ({ runGroups }, testInfo) => {
|
|||
test('should stop project if setup fails', async ({ runGroups }, testInfo) => {
|
||||
const projectTemplates = {
|
||||
'a': {
|
||||
setup: ['**/*.setup.ts']
|
||||
_setup: ['**/*.setup.ts']
|
||||
},
|
||||
'b': {
|
||||
setup: /.*b.setup.ts/
|
||||
_setup: /.*b.setup.ts/
|
||||
},
|
||||
};
|
||||
const configWithFiles = createConfigWithProjects(['a', 'b', 'c'], testInfo, projectTemplates);
|
||||
|
|
@ -162,7 +162,7 @@ test('should run setup in each project shard', async ({ runGroups }, testInfo) =
|
|||
projects: [
|
||||
{
|
||||
name: 'p1',
|
||||
setup: /.*.setup.ts/,
|
||||
_setup: /.*.setup.ts/,
|
||||
},
|
||||
]
|
||||
};`,
|
||||
|
|
@ -210,12 +210,12 @@ test('should run setup only for projects that have tests in the shard', async ({
|
|||
projects: [
|
||||
{
|
||||
name: 'p1',
|
||||
setup: /.*p1.setup.ts$/,
|
||||
_setup: /.*p1.setup.ts$/,
|
||||
testMatch: /.*a.test.ts/,
|
||||
},
|
||||
{
|
||||
name: 'p2',
|
||||
setup: /.*p2.setup.ts$/,
|
||||
_setup: /.*p2.setup.ts$/,
|
||||
testMatch: /.*b.test.ts/,
|
||||
},
|
||||
]
|
||||
|
|
@ -265,10 +265,10 @@ test('should run setup only for projects that have tests in the shard', async ({
|
|||
test('--project only runs setup from that project;', async ({ runGroups }, testInfo) => {
|
||||
const projectTemplates = {
|
||||
'a': {
|
||||
setup: /.*a.setup.ts/
|
||||
_setup: /.*a.setup.ts/
|
||||
},
|
||||
'b': {
|
||||
setup: /.*b.setup.ts/
|
||||
_setup: /.*b.setup.ts/
|
||||
},
|
||||
};
|
||||
const configWithFiles = createConfigWithProjects(['a', 'b', 'c'], testInfo, projectTemplates);
|
||||
|
|
@ -285,7 +285,7 @@ test('same file cannot be a setup and a test in the same project', async ({ runG
|
|||
projects: [
|
||||
{
|
||||
name: 'p1',
|
||||
setup: /.*a.test.ts$/,
|
||||
_setup: /.*a.test.ts$/,
|
||||
testMatch: /.*a.test.ts$/,
|
||||
},
|
||||
]
|
||||
|
|
@ -308,12 +308,12 @@ test('same file cannot be a setup and a test in different projects', async ({ ru
|
|||
projects: [
|
||||
{
|
||||
name: 'p1',
|
||||
setup: /.*a.test.ts$/,
|
||||
_setup: /.*a.test.ts$/,
|
||||
testMatch: /.*noMatch.test.ts$/,
|
||||
},
|
||||
{
|
||||
name: 'p2',
|
||||
setup: /.*noMatch.test.ts$/,
|
||||
_setup: /.*noMatch.test.ts$/,
|
||||
testMatch: /.*a.test.ts$/
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -24,14 +24,14 @@ test('should provide storage fixture', async ({ runInlineTest }) => {
|
|||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('should store number', async ({ }) => {
|
||||
const storage = test.info().storage();
|
||||
const storage = test.info()._storage();
|
||||
expect(storage).toBeTruthy();
|
||||
expect(await storage.get('number')).toBe(undefined);
|
||||
await storage.set('number', 2022)
|
||||
expect(await storage.get('number')).toBe(2022);
|
||||
});
|
||||
test('should store object', async ({ }) => {
|
||||
const storage = test.info().storage();
|
||||
const storage = test.info()._storage();
|
||||
expect(storage).toBeTruthy();
|
||||
expect(await storage.get('object')).toBe(undefined);
|
||||
await storage.set('object', { 'a': 2022 })
|
||||
|
|
@ -50,7 +50,7 @@ test('should share storage state between project setup and tests', async ({ runI
|
|||
projects: [
|
||||
{
|
||||
name: 'p1',
|
||||
setup: /.*storage.setup.ts/
|
||||
_setup: /.*storage.setup.ts/
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -58,7 +58,7 @@ test('should share storage state between project setup and tests', async ({ runI
|
|||
'storage.setup.ts': `
|
||||
const { test, expect } = pwt;
|
||||
test('should initialize storage', async ({ }) => {
|
||||
const storage = test.info().storage();
|
||||
const storage = test.info()._storage();
|
||||
expect(await storage.get('number')).toBe(undefined);
|
||||
await storage.set('number', 2022)
|
||||
expect(await storage.get('number')).toBe(2022);
|
||||
|
|
@ -71,7 +71,7 @@ test('should share storage state between project setup and tests', async ({ runI
|
|||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('should get data from setup', async ({ }) => {
|
||||
const storage = test.info().storage();
|
||||
const storage = test.info()._storage();
|
||||
expect(await storage.get('number')).toBe(2022);
|
||||
expect(await storage.get('object')).toEqual({ 'a': 2022 });
|
||||
});
|
||||
|
|
@ -79,7 +79,7 @@ test('should share storage state between project setup and tests', async ({ runI
|
|||
'b.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('should get data from setup', async ({ }) => {
|
||||
const storage = test.info().storage();
|
||||
const storage = test.info()._storage();
|
||||
expect(await storage.get('number')).toBe(2022);
|
||||
expect(await storage.get('object')).toEqual({ 'a': 2022 });
|
||||
});
|
||||
|
|
@ -97,7 +97,7 @@ test('should persist storage state between project runs', async ({ runInlineTest
|
|||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('should have no data on first run', async ({ }) => {
|
||||
const storage = test.info().storage();
|
||||
const storage = test.info()._storage();
|
||||
expect(await storage.get('number')).toBe(undefined);
|
||||
await storage.set('number', 2022)
|
||||
expect(await storage.get('object')).toBe(undefined);
|
||||
|
|
@ -107,7 +107,7 @@ test('should persist storage state between project runs', async ({ runInlineTest
|
|||
'b.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('should get data from previous run', async ({ }) => {
|
||||
const storage = test.info().storage();
|
||||
const storage = test.info()._storage();
|
||||
expect(await storage.get('number')).toBe(2022);
|
||||
expect(await storage.get('object')).toEqual({ 'a': 2022 });
|
||||
});
|
||||
|
|
@ -132,11 +132,11 @@ test('should isolate storage state between projects', async ({ runInlineTest })
|
|||
projects: [
|
||||
{
|
||||
name: 'p1',
|
||||
setup: /.*storage.setup.ts/
|
||||
_setup: /.*storage.setup.ts/
|
||||
},
|
||||
{
|
||||
name: 'p2',
|
||||
setup: /.*storage.setup.ts/
|
||||
_setup: /.*storage.setup.ts/
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -144,7 +144,7 @@ test('should isolate storage state between projects', async ({ runInlineTest })
|
|||
'storage.setup.ts': `
|
||||
const { test, expect } = pwt;
|
||||
test('should initialize storage', async ({ }) => {
|
||||
const storage = test.info().storage();
|
||||
const storage = test.info()._storage();
|
||||
expect(await storage.get('number')).toBe(undefined);
|
||||
await storage.set('number', 2022)
|
||||
expect(await storage.get('number')).toBe(2022);
|
||||
|
|
@ -157,7 +157,7 @@ test('should isolate storage state between projects', async ({ runInlineTest })
|
|||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('should get data from setup', async ({ }) => {
|
||||
const storage = test.info().storage();
|
||||
const storage = test.info()._storage();
|
||||
expect(await storage.get('number')).toBe(2022);
|
||||
expect(await storage.get('name')).toBe('str-' + test.info().project.name);
|
||||
});
|
||||
|
|
@ -165,7 +165,7 @@ test('should isolate storage state between projects', async ({ runInlineTest })
|
|||
'b.test.ts': `
|
||||
const { test } = pwt;
|
||||
test('should get data from setup', async ({ }) => {
|
||||
const storage = test.info().storage();
|
||||
const storage = test.info()._storage();
|
||||
expect(await storage.get('number')).toBe(2022);
|
||||
expect(await storage.get('name')).toBe('str-' + test.info().project.name);
|
||||
});
|
||||
|
|
@ -186,7 +186,7 @@ test('should load context storageState from storage', async ({ runInlineTest, se
|
|||
projects: [
|
||||
{
|
||||
name: 'p1',
|
||||
setup: /.*storage.setup.ts/
|
||||
_setup: /.*storage.setup.ts/
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -194,7 +194,7 @@ test('should load context storageState from storage', async ({ runInlineTest, se
|
|||
'storage.setup.ts': `
|
||||
const { test, expect } = pwt;
|
||||
test('should save storageState', async ({ page, context }) => {
|
||||
const storage = test.info().storage();
|
||||
const storage = test.info()._storage();
|
||||
expect(await storage.get('user')).toBe(undefined);
|
||||
await page.goto('${server.PREFIX}/setcookie.html');
|
||||
const state = await page.context().storageState();
|
||||
|
|
@ -204,7 +204,7 @@ test('should load context storageState from storage', async ({ runInlineTest, se
|
|||
'a.test.ts': `
|
||||
const { test } = pwt;
|
||||
test.use({
|
||||
storageStateName: 'user'
|
||||
_storageStateName: 'user'
|
||||
})
|
||||
test('should get data from setup', async ({ page }) => {
|
||||
await page.goto('${server.EMPTY_PAGE}');
|
||||
|
|
@ -225,7 +225,7 @@ test('should load context storageState from storage', async ({ runInlineTest, se
|
|||
expect(result.passed).toBe(3);
|
||||
});
|
||||
|
||||
test('should load storageStateName specified in the project config from storage', async ({ runInlineTest, server }) => {
|
||||
test('should load _storageStateName specified in the project config from storage', async ({ runInlineTest, server }) => {
|
||||
server.setRoute('/setcookie.html', (req, res) => {
|
||||
res.setHeader('Set-Cookie', ['a=v1']);
|
||||
res.end();
|
||||
|
|
@ -236,9 +236,9 @@ test('should load storageStateName specified in the project config from storage'
|
|||
projects: [
|
||||
{
|
||||
name: 'p1',
|
||||
setup: /.*storage.setup.ts/,
|
||||
_setup: /.*storage.setup.ts/,
|
||||
use: {
|
||||
storageStateName: 'stateInStorage',
|
||||
_storageStateName: 'stateInStorage',
|
||||
},
|
||||
}
|
||||
]
|
||||
|
|
@ -247,10 +247,10 @@ test('should load storageStateName specified in the project config from storage'
|
|||
'storage.setup.ts': `
|
||||
const { test, expect } = pwt;
|
||||
test.use({
|
||||
storageStateName: ({}, use) => use(undefined),
|
||||
_storageStateName: ({}, use) => use(undefined),
|
||||
})
|
||||
test('should save storageState', async ({ page, context }) => {
|
||||
const storage = test.info().storage();
|
||||
const storage = test.info()._storage();
|
||||
expect(await storage.get('stateInStorage')).toBe(undefined);
|
||||
await page.goto('${server.PREFIX}/setcookie.html');
|
||||
const state = await page.context().storageState();
|
||||
|
|
@ -270,7 +270,7 @@ test('should load storageStateName specified in the project config from storage'
|
|||
expect(result.passed).toBe(2);
|
||||
});
|
||||
|
||||
test('should load storageStateName specified in the global config from storage', async ({ runInlineTest, server }) => {
|
||||
test('should load _storageStateName specified in the global config from storage', async ({ runInlineTest, server }) => {
|
||||
server.setRoute('/setcookie.html', (req, res) => {
|
||||
res.setHeader('Set-Cookie', ['a=v1']);
|
||||
res.end();
|
||||
|
|
@ -279,12 +279,12 @@ test('should load storageStateName specified in the global config from storage',
|
|||
'playwright.config.js': `
|
||||
module.exports = {
|
||||
use: {
|
||||
storageStateName: 'stateInStorage',
|
||||
_storageStateName: 'stateInStorage',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'p1',
|
||||
setup: /.*storage.setup.ts/,
|
||||
_setup: /.*storage.setup.ts/,
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -292,10 +292,10 @@ test('should load storageStateName specified in the global config from storage',
|
|||
'storage.setup.ts': `
|
||||
const { test, expect } = pwt;
|
||||
test.use({
|
||||
storageStateName: ({}, use) => use(undefined),
|
||||
_storageStateName: ({}, use) => use(undefined),
|
||||
})
|
||||
test('should save storageStateName', async ({ page, context }) => {
|
||||
const storage = test.info().storage();
|
||||
test('should save _storageStateName', async ({ page, context }) => {
|
||||
const storage = test.info()._storage();
|
||||
expect(await storage.get('stateInStorage')).toBe(undefined);
|
||||
await page.goto('${server.PREFIX}/setcookie.html');
|
||||
const state = await page.context().storageState();
|
||||
|
|
@ -315,7 +315,7 @@ test('should load storageStateName specified in the global config from storage',
|
|||
expect(result.passed).toBe(2);
|
||||
});
|
||||
|
||||
test('should throw on unknown storageStateName value', async ({ runInlineTest, server }) => {
|
||||
test('should throw on unknown _storageStateName value', async ({ runInlineTest, server }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.js': `
|
||||
module.exports = {
|
||||
|
|
@ -323,7 +323,7 @@ test('should throw on unknown storageStateName value', async ({ runInlineTest, s
|
|||
{
|
||||
name: 'p1',
|
||||
use: {
|
||||
storageStateName: 'stateInStorage',
|
||||
_storageStateName: 'stateInStorage',
|
||||
},
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -188,18 +188,3 @@ test('config should allow void/empty options', async ({ runTSC }) => {
|
|||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should provide storage interface', async ({ runTSC }) => {
|
||||
const result = await runTSC({
|
||||
'a.spec.ts': `
|
||||
const { test } = pwt;
|
||||
test('my test', async () => {
|
||||
await test.info().storage().set('foo', 'bar');
|
||||
const val = await test.info().storage().get('foo');
|
||||
// @ts-expect-error
|
||||
await test.info().storage().unknown();
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import type { TraceViewerFixtures } from '../config/traceViewerFixtures';
|
|||
import { traceViewerFixtures } from '../config/traceViewerFixtures';
|
||||
export { expect } from '@playwright/test';
|
||||
import { TestChildProcess } from '../config/commonFixtures';
|
||||
import { DEFAULT_ARGS } from '../../packages/playwright-core/lib/server/chromium/chromium';
|
||||
import { chromiumSwitches } from '../../packages/playwright-core/lib/server/chromium/chromiumSwitches';
|
||||
|
||||
export const webView2Test = baseTest.extend<TraceViewerFixtures>(traceViewerFixtures).extend<PageTestFixtures, PageWorkerFixtures>({
|
||||
browserVersion: [process.env.PWTEST_WEBVIEW2_CHROMIUM_VERSION, { scope: 'worker' }],
|
||||
|
|
@ -39,7 +39,7 @@ export const webView2Test = baseTest.extend<TraceViewerFixtures>(traceViewerFixt
|
|||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: `--remote-debugging-port=${cdpPort} ${DEFAULT_ARGS.join(' ')}`,
|
||||
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: `--remote-debugging-port=${cdpPort} ${chromiumSwitches.join(' ')}`,
|
||||
WEBVIEW2_USER_DATA_FOLDER: path.join(fs.realpathSync.native(os.tmpdir()), `playwright-webview2-tests/user-data-dir-${testInfo.workerIndex}`),
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ set -x
|
|||
|
||||
trap "cd $(pwd -P)" EXIT
|
||||
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
|
||||
NODE_VERSION="18.12.1" # autogenerated via ./update-playwright-driver-version.mjs
|
||||
NODE_VERSION="16.18.0" # autogenerated via ./update-playwright-driver-version.mjs
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
PACKAGE_VERSION=$(node -p "require('../../package.json').version")
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-focal"
|
|||
# === INSTALL Node.js ===
|
||||
|
||||
RUN apt-get update && \
|
||||
# Install node18
|
||||
# Install node16
|
||||
apt-get install -y curl wget gpg && \
|
||||
curl -sL https://deb.nodesource.com/setup_18.x | bash - && \
|
||||
curl -sL https://deb.nodesource.com/setup_16.x | bash - && \
|
||||
apt-get install -y nodejs && \
|
||||
# Feature-parity with node.js base images.
|
||||
apt-get install -y --no-install-recommends git openssh-client && \
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-jammy"
|
|||
# === INSTALL Node.js ===
|
||||
|
||||
RUN apt-get update && \
|
||||
# Install node18
|
||||
# Install node16
|
||||
apt-get install -y curl wget gpg && \
|
||||
curl -sL https://deb.nodesource.com/setup_18.x | bash - && \
|
||||
curl -sL https://deb.nodesource.com/setup_16.x | bash - && \
|
||||
apt-get install -y nodejs && \
|
||||
# Feature-parity with node.js base images.
|
||||
apt-get install -y --no-install-recommends git openssh-client && \
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@ async function run() {
|
|||
|
||||
// Patch docker version in docs
|
||||
{
|
||||
const regex = new RegExp("(mcr.microsoft.com/playwright[^: ]*):?([^ ]*)");
|
||||
for (const filePath of getAllMarkdownFiles(path.join(PROJECT_DIR, 'docs'))) {
|
||||
let content = fs.readFileSync(filePath).toString();
|
||||
content = content.replace(new RegExp('(mcr.microsoft.com/playwright[^:]*):([\\w\\d-.]+)', 'ig'), (match, imageName, imageVersion) => {
|
||||
|
|
@ -165,6 +164,9 @@ async function run() {
|
|||
|
||||
// This validates member links.
|
||||
documentation.setLinkRenderer(() => undefined);
|
||||
// This validates code snippet groups in comments.
|
||||
documentation.setCodeGroupsTransformer(lang, tabs => tabs.map(tab => tab.spec));
|
||||
documentation.generateSourceCodeComments();
|
||||
|
||||
const relevantMarkdownFiles = new Set([...getAllMarkdownFiles(documentationRoot)
|
||||
// filter out language specific files
|
||||
|
|
@ -185,9 +187,12 @@ async function run() {
|
|||
if (langs.some(other => other !== lang && filePath.endsWith(`-${other}.md`)))
|
||||
continue;
|
||||
const data = fs.readFileSync(filePath, 'utf-8');
|
||||
const rootNode = md.filterNodesForLanguage(md.parse(data), lang);
|
||||
let rootNode = md.filterNodesForLanguage(md.parse(data), lang);
|
||||
// Validates code snippet groups.
|
||||
rootNode = md.processCodeGroups(rootNode, lang, tabs => tabs.map(tab => tab.spec));
|
||||
// Renders links.
|
||||
documentation.renderLinksInText(rootNode);
|
||||
// Validate links
|
||||
// Validate links.
|
||||
{
|
||||
md.visitAll(rootNode, node => {
|
||||
if (!node.text)
|
||||
|
|
|
|||
|
|
@ -59,6 +59,12 @@ const md = require('../markdown');
|
|||
* }} Metainfo
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* csharpOptionOverloadsShortNotation?: boolean,
|
||||
* }} LanguageOptions
|
||||
*/
|
||||
|
||||
class Documentation {
|
||||
/**
|
||||
* @param {!Array<!Documentation.Class>} classesArray
|
||||
|
|
@ -104,13 +110,14 @@ class Documentation {
|
|||
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {LanguageOptions=} options
|
||||
*/
|
||||
filterForLanguage(lang) {
|
||||
filterForLanguage(lang, options = {}) {
|
||||
const classesArray = [];
|
||||
for (const clazz of this.classesArray) {
|
||||
if (clazz.langs.only && !clazz.langs.only.includes(lang))
|
||||
continue;
|
||||
clazz.filterForLanguage(lang);
|
||||
clazz.filterForLanguage(lang, options);
|
||||
classesArray.push(clazz);
|
||||
}
|
||||
this.classesArray = classesArray;
|
||||
|
|
@ -165,9 +172,23 @@ class Documentation {
|
|||
this._patchLinks?.(null, nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {import('../markdown').CodeGroupTransformer} transformer
|
||||
*/
|
||||
setCodeGroupsTransformer(lang, transformer) {
|
||||
this._codeGroupsTransformer = { lang, transformer };
|
||||
}
|
||||
|
||||
generateSourceCodeComments() {
|
||||
for (const clazz of this.classesArray)
|
||||
clazz.visit(item => item.comment = generateSourceCodeComment(item.spec));
|
||||
for (const clazz of this.classesArray) {
|
||||
clazz.visit(item => {
|
||||
let spec = item.spec;
|
||||
if (spec && this._codeGroupsTransformer)
|
||||
spec = md.processCodeGroups(spec, this._codeGroupsTransformer.lang, this._codeGroupsTransformer.transformer);
|
||||
item.comment = generateSourceCodeComment(spec);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clone() {
|
||||
|
|
@ -245,13 +266,14 @@ Documentation.Class = class {
|
|||
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {LanguageOptions=} options
|
||||
*/
|
||||
filterForLanguage(lang) {
|
||||
filterForLanguage(lang, options = {}) {
|
||||
const membersArray = [];
|
||||
for (const member of this.membersArray) {
|
||||
if (member.langs.only && !member.langs.only.includes(lang))
|
||||
continue;
|
||||
member.filterForLanguage(lang);
|
||||
member.filterForLanguage(lang, options);
|
||||
membersArray.push(member);
|
||||
}
|
||||
this.membersArray = membersArray;
|
||||
|
|
@ -394,29 +416,39 @@ Documentation.Member = class {
|
|||
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {LanguageOptions=} options
|
||||
*/
|
||||
filterForLanguage(lang) {
|
||||
filterForLanguage(lang, options = {}) {
|
||||
if (!this.type)
|
||||
return;
|
||||
if (this.langs.aliases && this.langs.aliases[lang])
|
||||
this.alias = this.langs.aliases[lang];
|
||||
if (this.langs.types && this.langs.types[lang])
|
||||
this.type = this.langs.types[lang];
|
||||
this.type.filterForLanguage(lang);
|
||||
this.type.filterForLanguage(lang, options);
|
||||
const argsArray = [];
|
||||
for (const arg of this.argsArray) {
|
||||
if (arg.langs.only && !arg.langs.only.includes(lang))
|
||||
continue;
|
||||
const overriddenArg = (arg.langs.overrides && arg.langs.overrides[lang]) || arg;
|
||||
overriddenArg.filterForLanguage(lang);
|
||||
overriddenArg.filterForLanguage(lang, options);
|
||||
// @ts-ignore
|
||||
if (overriddenArg.name === 'options' && !overriddenArg.type.properties.length)
|
||||
continue;
|
||||
// @ts-ignore
|
||||
overriddenArg.type.filterForLanguage(lang);
|
||||
overriddenArg.type.filterForLanguage(lang, options);
|
||||
argsArray.push(overriddenArg);
|
||||
}
|
||||
this.argsArray = argsArray;
|
||||
|
||||
const optionsArg = this.argsArray.find(arg => arg.name === 'options');
|
||||
if (lang === 'csharp' && optionsArg) {
|
||||
try {
|
||||
patchCSharpOptionOverloads(optionsArg, options);
|
||||
} catch (e) {
|
||||
throw new Error(`Error processing csharp options in ${this.clazz?.name}.${this.name}: ` + e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterOutExperimental() {
|
||||
|
|
@ -628,15 +660,16 @@ Documentation.Type = class {
|
|||
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {LanguageOptions=} options
|
||||
*/
|
||||
filterForLanguage(lang) {
|
||||
filterForLanguage(lang, options = {}) {
|
||||
if (!this.properties)
|
||||
return;
|
||||
const properties = [];
|
||||
for (const prop of this.properties) {
|
||||
if (prop.langs.only && !prop.langs.only.includes(lang))
|
||||
continue;
|
||||
prop.filterForLanguage(lang);
|
||||
prop.filterForLanguage(lang, options);
|
||||
properties.push(prop);
|
||||
}
|
||||
this.properties = properties;
|
||||
|
|
@ -814,8 +847,6 @@ function patchLinks(classOrMember, spec, classesMap, membersMap, linkRenderer) {
|
|||
function generateSourceCodeComment(spec) {
|
||||
const comments = (spec || []).filter(n => !n.type.startsWith('h') && (n.type !== 'li' || n.liType !== 'default')).map(c => md.clone(c));
|
||||
md.visitAll(comments, node => {
|
||||
if (node.codeLang && node.codeLang.includes('tab=js-js'))
|
||||
node.type = 'null';
|
||||
if (node.type === 'li' && node.liType === 'bullet')
|
||||
node.liType = 'default';
|
||||
if (node.type === 'note') {
|
||||
|
|
@ -827,4 +858,73 @@ function generateSourceCodeComment(spec) {
|
|||
return md.render(comments, 120);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Documentation.Member} optionsArg
|
||||
* @param {LanguageOptions=} options
|
||||
*/
|
||||
function patchCSharpOptionOverloads(optionsArg, options = {}) {
|
||||
const props = optionsArg.type?.properties;
|
||||
if (!props)
|
||||
return;
|
||||
const propsToDelete = new Set();
|
||||
const propsToAdd = [];
|
||||
for (const prop of props) {
|
||||
const union = prop.type?.union;
|
||||
if (!union)
|
||||
continue;
|
||||
const isEnum = union[0].name.startsWith('"');
|
||||
const isNullable = union.length === 2 && union.some(type => type.name === 'null');
|
||||
if (isEnum || isNullable)
|
||||
continue;
|
||||
|
||||
const shortNotation = [];
|
||||
propsToDelete.add(prop);
|
||||
for (const type of union) {
|
||||
const suffix = csharpOptionOverloadSuffix(prop.name, type.name);
|
||||
if (options.csharpOptionOverloadsShortNotation) {
|
||||
if (type.name === 'string')
|
||||
shortNotation.push(prop.alias);
|
||||
else
|
||||
shortNotation.push(prop.alias + suffix);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newProp = prop.clone();
|
||||
newProp.name = prop.name + suffix;
|
||||
newProp.alias = prop.alias + suffix;
|
||||
newProp.type = type;
|
||||
propsToAdd.push(newProp);
|
||||
|
||||
if (type.name === 'string') {
|
||||
const stringProp = prop.clone();
|
||||
stringProp.type = type;
|
||||
propsToAdd.push(stringProp);
|
||||
}
|
||||
}
|
||||
if (options.csharpOptionOverloadsShortNotation) {
|
||||
const newProp = prop.clone();
|
||||
newProp.alias = newProp.name = shortNotation.join('|');
|
||||
propsToAdd.push(newProp);
|
||||
}
|
||||
}
|
||||
for (const prop of propsToDelete)
|
||||
props.splice(props.indexOf(prop), 1);
|
||||
props.push(...propsToAdd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} option
|
||||
* @param {string} type
|
||||
*/
|
||||
function csharpOptionOverloadSuffix(option, type) {
|
||||
switch (type) {
|
||||
case 'string': return 'String';
|
||||
case 'RegExp': return 'Regex';
|
||||
case 'function': return 'Func';
|
||||
case 'Buffer': return 'Byte';
|
||||
case 'Serializable': return 'Object';
|
||||
}
|
||||
throw new Error(`CSharp option "${option}" has unsupported type overload "${type}"`);
|
||||
}
|
||||
|
||||
module.exports = Documentation;
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ function renderConstructors(name, type, out) {
|
|||
function renderMember(member, parent, options, out) {
|
||||
const name = toMemberName(member);
|
||||
if (member.kind === 'method') {
|
||||
renderMethod(member, parent, name, { mode: 'options', trimRunAndPrefix: options.trimRunAndPrefix }, out);
|
||||
renderMethod(member, parent, name, { trimRunAndPrefix: options.trimRunAndPrefix }, out);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -348,12 +348,6 @@ function getPropertyOverloads(type, member, name, parent) {
|
|||
if (member.type.expression === '[string]|[float]')
|
||||
jsonName = `${member.name}String`;
|
||||
overloads.push({ type, name, jsonName });
|
||||
} else {
|
||||
for (const overload of member.type.union) {
|
||||
const t = translateType(overload, parent, t => generateNameDefault(member, name, t, parent));
|
||||
const suffix = toOverloadSuffix(t);
|
||||
overloads.push({ type: t, name: name + suffix, jsonName: member.name + suffix });
|
||||
}
|
||||
}
|
||||
return overloads;
|
||||
}
|
||||
|
|
@ -463,7 +457,6 @@ function generateEnumNameIfApplicable(type) {
|
|||
* @param {Documentation.Class | Documentation.Type} parent
|
||||
* @param {string} name
|
||||
* @param {{
|
||||
* mode: 'options'|'named'|'base',
|
||||
* nodocs?: boolean,
|
||||
* abstract?: boolean,
|
||||
* public?: boolean,
|
||||
|
|
@ -556,16 +549,12 @@ function renderMethod(member, parent, name, options, out) {
|
|||
return;
|
||||
|
||||
if (arg.name === 'options') {
|
||||
if (options.mode === 'options' || options.mode === 'base') {
|
||||
const optionsType = rewriteSuggestedOptionsName(member.clazz.name + name.replace('<T>', '') + 'Options');
|
||||
if (!optionTypes.has(optionsType) || arg.type.properties.length > optionTypes.get(optionsType).properties.length)
|
||||
optionTypes.set(optionsType, arg.type);
|
||||
args.push(`${optionsType}? options = default`);
|
||||
argTypeMap.set(`${optionsType}? options = default`, 'options');
|
||||
addParamsDoc('options', ['Call options']);
|
||||
} else {
|
||||
arg.type.properties.forEach(processArg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -632,37 +621,6 @@ function renderMethod(member, parent, name, options, out) {
|
|||
.sort((a, b) => b.alias === 'options' ? -1 : 0) // move options to the back to the arguments list
|
||||
.forEach(processArg);
|
||||
|
||||
let body = ';';
|
||||
if (options.mode === 'base') {
|
||||
// Generate options -> named transition.
|
||||
const tokens = [];
|
||||
for (const arg of member.argsArray) {
|
||||
if (arg.name === 'action' && options.trimRunAndPrefix)
|
||||
continue;
|
||||
if (arg.name !== 'options') {
|
||||
tokens.push(toArgumentName(arg.name));
|
||||
continue;
|
||||
}
|
||||
for (const opt of arg.type.properties) {
|
||||
// TODO: use translate type here?
|
||||
if (opt.type.union && !opt.type.union[0].name.startsWith('"') && opt.type.union[0].name !== 'null' && opt.type.expression !== '[string]|[Buffer]') {
|
||||
// Explode overloads.
|
||||
for (const t of opt.type.union) {
|
||||
const suffix = toOverloadSuffix(translateType(t, parent));
|
||||
tokens.push(`${opt.name}${suffix}: options.${toMemberName(opt)}${suffix}`);
|
||||
}
|
||||
} else {
|
||||
tokens.push(`${opt.alias || opt.name}: options.${toMemberName(opt)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
body = `
|
||||
{
|
||||
options ??= new ${member.clazz.name}${name}Options();
|
||||
return ${toAsync(name, member.async)}(${tokens.join(', ')});
|
||||
}`;
|
||||
}
|
||||
|
||||
if (!explodedArgs.length) {
|
||||
if (!options.nodocs) {
|
||||
out.push(...XmlDoc.renderXmlDoc(member.spec, maxDocumentationColumnWidth));
|
||||
|
|
@ -670,7 +628,7 @@ function renderMethod(member, parent, name, options, out) {
|
|||
}
|
||||
if (member.deprecated)
|
||||
out.push(`[System.Obsolete]`);
|
||||
out.push(`${modifiers}${type} ${toAsync(name, member.async)}(${args.join(', ')})${body}`);
|
||||
out.push(`${modifiers}${type} ${toAsync(name, member.async)}(${args.join(', ')});`);
|
||||
} else {
|
||||
let containsOptionalExplodedArgs = false;
|
||||
explodedArgs.forEach((explodedArg, argIndex) => {
|
||||
|
|
@ -692,7 +650,7 @@ function renderMethod(member, parent, name, options, out) {
|
|||
overloadedArgs.push(arg);
|
||||
}
|
||||
}
|
||||
out.push(`${modifiers}${type} ${toAsync(name, member.async)}(${overloadedArgs.join(', ')})${body}`);
|
||||
out.push(`${modifiers}${type} ${toAsync(name, member.async)}(${overloadedArgs.join(', ')});`);
|
||||
if (argIndex < explodedArgs.length - 1)
|
||||
out.push(''); // output a special blank line
|
||||
});
|
||||
|
|
@ -712,7 +670,7 @@ function renderMethod(member, parent, name, options, out) {
|
|||
if (!options.nodocs)
|
||||
printArgDoc(argType, paramDocs.get(argType), out);
|
||||
});
|
||||
out.push(`${type} ${name}(${filteredArgs.join(', ')})${body}`);
|
||||
out.push(`${type} ${name}(${filteredArgs.join(', ')});`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -874,14 +832,6 @@ function printArgDoc(name, value, out) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} typeName
|
||||
* @return {string}
|
||||
*/
|
||||
function toOverloadSuffix(typeName) {
|
||||
return toTitleCase(typeName.replace(/[<].*[>]/, '').replace(/[^a-zA-Z]/g, ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {boolean} convert
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
//@ts-check
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const toKebabCase = require('lodash/kebabCase')
|
||||
const devices = require('../../packages/playwright-core/lib/server/deviceDescriptors');
|
||||
const Documentation = require('../doclint/documentation');
|
||||
|
|
@ -87,6 +86,7 @@ class TypesGenerator {
|
|||
return createMarkdownLink(member, `${className}${member.alias}`);
|
||||
throw new Error('Unknown member kind ' + member.kind);
|
||||
});
|
||||
this.documentation.setCodeGroupsTransformer('js', tabs => tabs.filter(tab => tab.value === 'ts').map(tab => tab.spec));
|
||||
this.documentation.generateSourceCodeComments();
|
||||
|
||||
const handledClasses = new Set();
|
||||
|
|
|
|||
6
utils/generate_types/overrides-test.d.ts
vendored
6
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -196,11 +196,6 @@ type ConnectOptions = {
|
|||
timeout?: number;
|
||||
};
|
||||
|
||||
export interface Storage {
|
||||
get<T>(name: string): Promise<T | undefined>;
|
||||
set<T>(name: string, value: T | undefined): Promise<void>;
|
||||
}
|
||||
|
||||
export interface PlaywrightWorkerOptions {
|
||||
browserName: BrowserName;
|
||||
defaultBrowserType: BrowserName;
|
||||
|
|
@ -233,7 +228,6 @@ export interface PlaywrightTestOptions {
|
|||
permissions: string[] | undefined;
|
||||
proxy: Proxy | undefined;
|
||||
storageState: StorageState | undefined;
|
||||
storageStateName: string | undefined;
|
||||
timezoneId: string | undefined;
|
||||
userAgent: string | undefined;
|
||||
viewport: ViewportSize | null | undefined;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ set +x
|
|||
# Install Node.js
|
||||
|
||||
apt-get update && apt-get install -y curl && \
|
||||
curl -sL https://deb.nodesource.com/setup_18.x | bash - && \
|
||||
curl -sL https://deb.nodesource.com/setup_16.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Install apt-file
|
||||
|
|
|
|||
|
|
@ -62,6 +62,12 @@
|
|||
* lines: string[],
|
||||
* }} MarkdownPropsNode */
|
||||
|
||||
/** @typedef {{
|
||||
* value: string, groupId: string, spec: MarkdownNode
|
||||
* }} CodeGroup */
|
||||
|
||||
/** @typedef {function(CodeGroup[]): MarkdownNode[]} CodeGroupTransformer */
|
||||
|
||||
/** @typedef {MarkdownTextNode | MarkdownLiNode | MarkdownCodeNode | MarkdownNoteNode | MarkdownHeaderNode | MarkdownNullNode | MarkdownPropsNode } MarkdownNode */
|
||||
|
||||
function flattenWrappedLines(content) {
|
||||
|
|
@ -307,7 +313,7 @@ function innerRenderMdNode(indent, node, lastNode, result, maxColumns) {
|
|||
if (process.env.API_JSON_MODE)
|
||||
result.push(`${indent}\`\`\`${node.codeLang}`);
|
||||
else
|
||||
result.push(`${indent}\`\`\`${codeLangToHighlighter(node.codeLang)}`);
|
||||
result.push(`${indent}\`\`\`${node.codeLang ? parseCodeLang(node.codeLang).highlighter : ''}`);
|
||||
for (const line of node.lines)
|
||||
result.push(indent + line);
|
||||
result.push(`${indent}\`\`\``);
|
||||
|
|
@ -469,13 +475,84 @@ function filterNodesForLanguage(nodes, language) {
|
|||
|
||||
/**
|
||||
* @param {string} codeLang
|
||||
* @return {string}
|
||||
* @return {{ highlighter: string, language: string|undefined, codeGroup: string|undefined}}
|
||||
*/
|
||||
function codeLangToHighlighter(codeLang) {
|
||||
const [lang] = codeLang.split(' ');
|
||||
if (lang === 'python')
|
||||
return 'py';
|
||||
return lang;
|
||||
function parseCodeLang(codeLang) {
|
||||
if (codeLang === 'python async')
|
||||
return { highlighter: 'py', codeGroup: 'python-async', language: 'python' };
|
||||
if (codeLang === 'python sync')
|
||||
return { highlighter: 'py', codeGroup: 'python-sync', language: 'python' };
|
||||
|
||||
const [highlighter] = codeLang.split(' ');
|
||||
if (!highlighter)
|
||||
throw new Error(`Cannot parse code block lang: "${codeLang}"`);
|
||||
|
||||
const languageMatch = codeLang.match(/ lang=([\w\d]+)/);
|
||||
let language = languageMatch ? languageMatch[1] : undefined;
|
||||
if (!language) {
|
||||
if (highlighter === 'ts')
|
||||
language = 'js';
|
||||
else if (highlighter === 'py')
|
||||
language = 'python';
|
||||
else if (['js', 'python', 'csharp', 'java'].includes(highlighter))
|
||||
language = highlighter;
|
||||
}
|
||||
|
||||
module.exports = { parse, render, clone, visitAll, visit, generateToc, filterNodesForLanguage, codeLangToHighlighter };
|
||||
const tabMatch = codeLang.match(/ tab=([\w\d-]+)/);
|
||||
return { highlighter, language, codeGroup: tabMatch ? tabMatch[1] : '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownNode[]} spec
|
||||
* @param {string} language
|
||||
* @param {CodeGroupTransformer} transformer
|
||||
* @returns {MarkdownNode[]}
|
||||
*/
|
||||
function processCodeGroups(spec, language, transformer) {
|
||||
/** @type {MarkdownNode[]} */
|
||||
const newSpec = [];
|
||||
for (let i = 0; i < spec.length; ++i) {
|
||||
/** @type {{value: string, groupId: string, spec: MarkdownNode}[]} */
|
||||
const tabs = [];
|
||||
for (;i < spec.length; i++) {
|
||||
const codeLang = spec[i].codeLang;
|
||||
if (!codeLang)
|
||||
break;
|
||||
let parsed;
|
||||
try {
|
||||
parsed = parseCodeLang(codeLang);
|
||||
} catch (e) {
|
||||
throw new Error(e.message + '\n while processing:\n' + render([spec[i]]));
|
||||
}
|
||||
if (!parsed.codeGroup)
|
||||
break;
|
||||
if (parsed.language && parsed.language !== language)
|
||||
continue;
|
||||
const [groupId, value] = parsed.codeGroup.split('-');
|
||||
tabs.push({ groupId, value, spec: spec[i] });
|
||||
}
|
||||
if (tabs.length) {
|
||||
if (tabs.length === 1)
|
||||
throw new Error(`Lonely tab "${tabs[0].spec.codeLang}". Make sure there are at least two tabs in the group.\n` + render([tabs[0].spec]));
|
||||
|
||||
// Validate group consistency.
|
||||
const groupId = tabs[0].groupId;
|
||||
const values = new Set();
|
||||
for (const tab of tabs) {
|
||||
if (tab.groupId !== groupId)
|
||||
throw new Error('Mixed group ids: ' + render(spec));
|
||||
if (values.has(tab.value))
|
||||
throw new Error(`Duplicated tab "${tab.value}"\n` + render(tabs.map(tab => tab.spec)));
|
||||
values.add(tab.value);
|
||||
}
|
||||
|
||||
// Append transformed nodes.
|
||||
newSpec.push(...transformer(tabs));
|
||||
}
|
||||
if (i < spec.length)
|
||||
newSpec.push(spec[i]);
|
||||
}
|
||||
return newSpec;
|
||||
}
|
||||
|
||||
module.exports = { parse, render, clone, visitAll, visit, generateToc, filterNodesForLanguage, parseCodeLang, processCodeGroups };
|
||||
|
|
|
|||
Loading…
Reference in a new issue