Merge branch 'main' into only-changed-no-tests-exit-1

This commit is contained in:
Simon Knott 2024-08-22 13:12:47 +02:00
commit 94b0181b22
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
69 changed files with 2247 additions and 998 deletions

View file

@ -6,9 +6,14 @@ module.exports = {
sourceType: "module",
},
extends: [
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
settings: {
react: { version: "18" }
},
/**
* ESLint rules
*
@ -124,5 +129,8 @@ module.exports = {
"mustMatch": "Copyright",
"templateFile": require("path").join(__dirname, "utils", "copyright.js"),
}],
// react
"react/react-in-jsx-scope": 0
}
};

View file

@ -415,13 +415,42 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
[`method: Page.addInitScript`] is not defined.
:::
**Bundling**
If you have a complex script split into several files, it needs to be bundled into a single file first. We recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a commonjs module and pass [`option: path`] and [`option: arg`].
```js browser title="mocks/mockRandom.ts"
// This script can import other files.
import { defaultValue } from './defaultValue';
export default function(value?: number) {
window.Math.random = () => value ?? defaultValue;
}
```
```sh
# bundle with esbuild
esbuild mocks/mockRandom.ts --bundle --format=cjs --outfile=mocks/mockRandom.js
```
```js title="tests/example.spec.ts"
const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') };
// Passing 42 as an argument to the default export function.
await context.addInitScript({ path: mockPath }, 42);
// Make sure to pass undefined even if you do not need to pass an argument.
// This instructs Playwright to treat the file as a commonjs module.
await context.addInitScript({ path: mockPath }, undefined);
```
### param: BrowserContext.addInitScript.script
* since: v1.8
* langs: js
- `script` <[function]|[string]|[Object]>
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
current working directory. Optional.
- `content` ?<[string]> Raw script content. Optional.
current working directory.
- `content` ?<[string]> Raw script content.
Script to be evaluated in all pages in the browser context.
@ -437,7 +466,9 @@ Script to be evaluated in all pages in the browser context.
* langs: js
- `arg` ?<[Serializable]>
Optional argument to pass to [`param: script`] (only supported when passing a function).
Optional JSON-serializable argument to pass to [`param: script`].
* When `script` is a function, the argument is passed to it directly.
* When `script` is a file path, the file is assumed to be a commonjs module. The default export, either `module.exports` or `module.exports.default`, should be a function that's going to be executed with this argument.
### param: BrowserContext.addInitScript.path
* since: v1.8

View file

@ -619,13 +619,42 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
[`method: Page.addInitScript`] is not defined.
:::
**Bundling**
If you have a complex script split into several files, it needs to be bundled into a single file first. We recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a commonjs module and pass [`option: path`] and [`option: arg`].
```js browser title="mocks/mockRandom.ts"
// This script can import other files.
import { defaultValue } from './defaultValue';
export default function(value?: number) {
window.Math.random = () => value ?? defaultValue;
}
```
```sh
# bundle with esbuild
esbuild mocks/mockRandom.ts --bundle --format=cjs --outfile=mocks/mockRandom.js
```
```js title="tests/example.spec.ts"
const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') };
// Passing 42 as an argument to the default export function.
await page.addInitScript({ path: mockPath }, 42);
// Make sure to pass undefined even if you do not need to pass an argument.
// This instructs Playwright to treat the file as a commonjs module.
await page.addInitScript({ path: mockPath }, undefined);
```
### param: Page.addInitScript.script
* since: v1.8
* langs: js
- `script` <[function]|[string]|[Object]>
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
current working directory. Optional.
- `content` ?<[string]> Raw script content. Optional.
current working directory.
- `content` ?<[string]> Raw script content.
Script to be evaluated in the page.
@ -641,7 +670,9 @@ Script to be evaluated in all pages in the browser context.
* langs: js
- `arg` ?<[Serializable]>
Optional argument to pass to [`param: script`] (only supported when passing a function).
Optional JSON-serializable argument to pass to [`param: script`].
* When `script` is a function, the argument is passed to it directly.
* When `script` is a file path, the file is assumed to be a commonjs module. The default export, either `module.exports` or `module.exports.default`, should be a function that's going to be executed with this argument.
### param: Page.addInitScript.path
* since: v1.8

View file

@ -531,15 +531,18 @@ Does not enforce fixed viewport, allows resizing window in the headed mode.
- `clientCertificates` <[Array]<[Object]>>
- `origin` <[string]> Exact origin that the certificate is valid for. Origin includes `https` protocol, a hostname and optionally a port.
- `certPath` ?<[path]> Path to the file with the certificate in PEM format.
- `cert` ?<[Buffer]> Direct value of the certificate in PEM format.
- `keyPath` ?<[path]> Path to the file with the private key in PEM format.
- `key` ?<[Buffer]> Direct value of the private key in PEM format.
- `pfxPath` ?<[path]> Path to the PFX or PKCS12 encoded private key and certificate chain.
- `pfx` ?<[Buffer]> Direct value of the PFX or PKCS12 encoded private key and certificate chain.
- `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX).
TLS Client Authentication allows the server to request a client certificate and verify it.
**Details**
An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for.
An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`, a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally, `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for.
:::note
Using Client Certificates in combination with Proxy Servers is not supported.

View file

@ -261,11 +261,11 @@ npx playwright --version
Setup CI/CD and run your tests frequently. The more often you run your tests the better. Ideally you should run your tests on each commit and pull request. Playwright comes with a [GitHub actions workflow](/ci-intro.md) so that tests will run on CI for you with no setup required. Playwright can also be setup on the [CI environment](/ci.md) of your choice.
Use Linux when running your tests on CI as it is cheaper. Developers can use whatever environment when running locally but use linux on CI.
Use Linux when running your tests on CI as it is cheaper. Developers can use whatever environment when running locally but use linux on CI. Consider setting up [Sharding](./test-sharding.md) to make CI faster.
### Lint your tests
Linting the tests helps catching errors early. Use [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises/) [ESLint](https://eslint.org) rule to make sure there are no missing awaits before the asynchronous calls to the Playwright API.
We recommend TypeScript and linting with ESLint for your tests to catch errors early. Use [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises/) [ESLint](https://eslint.org) rule to make sure there are no missing awaits before the asynchronous calls to the Playwright API. On your CI you can run `tsc --noEmit` to ensure that functions are called with the right signature.
### Use parallelism and sharding

View file

@ -461,7 +461,7 @@ Playwright's Firefox version matches the recent [Firefox Stable](https://www.moz
### WebKit
Playwright's WebKit is derived from the latest WebKit main branch sources, often before these updates are incorporated into Apple Safari and other WebKit-based browsers. This gives a lot of lead time to react on the potential browser update issues. Playwright doesn't work with the branded version of Safari since it relies on patches. Instead, you can test using the most recent WebKit build. Note that avialability of certain features, which depend heavily on the underlying platform, may vary between operating systems.
Playwright's WebKit is derived from the latest WebKit main branch sources, often before these updates are incorporated into Apple Safari and other WebKit-based browsers. This gives a lot of lead time to react on the potential browser update issues. Playwright doesn't work with the branded version of Safari since it relies on patches. Instead, you can test using the most recent WebKit build. Note that availability of certain features, which depend heavily on the underlying platform, may vary between operating systems.
## Install behind a firewall or a proxy

View file

@ -1,19 +1,17 @@
---
id: ci-intro
title: "CI GitHub Actions"
title: "Setting up CI"
---
## Introduction
* langs: js
Playwright tests can be run on any CI provider. In this section we will cover running tests on GitHub using GitHub actions. If you would like to see how to configure other CI providers check out our detailed [doc on Continuous Integration](./ci.md).
When [installing Playwright](./intro.md) using the [VS Code extension](./getting-started-vscode.md) or with `npm init playwright@latest` you are given the option to add a [GitHub Actions](https://docs.github.com/en/actions) workflow. This creates a `playwright.yml` file inside a `.github/workflows` folder containing everything you need so that your tests run on each push and pull request into the main/master branch.
Playwright tests can be run on any CI provider. This guide covers one way of running tests on GitHub using GitHub actions. If you would like to learn more, or how to configure other CI providers, check out our detailed [doc on Continuous Integration](./ci.md).
#### You will learn
* langs: js
- [How to run tests on push/pull_request](/ci-intro.md#on-pushpull_request)
- [How to set up GitHub Actions](/ci-intro.md#setting-up-github-actions)
- [How to view test logs](/ci-intro.md#viewing-test-logs)
- [How to view the HTML report](/ci-intro.md#viewing-the-html-report)
- [How to view the trace](/ci-intro.md#viewing-the-trace)
@ -25,22 +23,18 @@ When [installing Playwright](./intro.md) using the [VS Code extension](./getting
Playwright tests can be ran on any CI provider. In this section we will cover running tests on GitHub using GitHub actions. If you would like to see how to configure other CI providers check out our detailed doc on Continuous Integration.
To add a [GitHub Actions](https://docs.github.com/en/actions) file first create `.github/workflows` folder and inside it add a `playwright.yml` file containing the example code below so that your tests will run on each push and pull request for the main/master branch.
#### You will learn
* langs: python, java, csharp
- [How to run tests on push/pull_request](/ci-intro.md#on-pushpull_request)
- [How to set up GitHub Actions](/ci-intro.md#setting-up-github-actions)
- [How to view test logs](/ci-intro.md#viewing-test-logs)
- [How to view the trace](/ci-intro.md#viewing-the-trace)
## Setting up GitHub Actions
### On push/pull_request
* langs: js
Tests will run on push or pull request on branches main/master. The [workflow](https://docs.github.com/en/actions/using-workflows/about-workflows) will install all dependencies, install Playwright and then run the tests. It will also create the HTML report.
When [installing Playwright](./intro.md) using the [VS Code extension](./getting-started-vscode.md) or with `npm init playwright@latest` you are given the option to add a [GitHub Actions](https://docs.github.com/en/actions) workflow. This creates a `playwright.yml` file inside a `.github/workflows` folder containing everything you need so that your tests run on each push and pull request into the main/master branch. Here's how that file looks:
```yml js title=".github/workflows/playwright.yml"
name: Playwright Tests
@ -57,7 +51,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
@ -72,10 +66,21 @@ jobs:
retention-days: 30
```
### On push/pull_request
The workflow performs these steps:
1. Clone your repository
2. Install Node.js
3. Install NPM Dependencies
4. Install Playwright Browsers
5. Run Playwright tests
6. Upload HTML report to the GitHub UI
To learn more about this, see ["Understanding GitHub Actions"](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions).
## Setting up GitHub Actions
* langs: python, java, csharp
Tests will run on push or pull request on branches main/master. The [workflow](https://docs.github.com/en/actions/using-workflows/about-workflows) will install all dependencies, install Playwright and then run the tests.
To add a [GitHub Actions](https://docs.github.com/en/actions) file first create `.github/workflows` folder and inside it add a `playwright.yml` file containing the example code below so that your tests will run on each push and pull request for the main/master branch.
```yml python title=".github/workflows/playwright.yml"
name: Playwright Tests
@ -128,7 +133,7 @@ jobs:
java-version: '17'
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Install Playwright
- name: Ensure browsers are installed
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps"
- name: Run tests
run: mvn test
@ -151,275 +156,23 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- run: dotnet build
- name: Build & Install
run: dotnet build
- name: Ensure browsers are installed
run: pwsh bin/Debug/net8.0/playwright.ps1 install --with-deps
- name: Run your tests
run: dotnet test
```
### On push/pull_request (sharded)
* langs: js
To learn more about this, see ["Understanding GitHub Actions"](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions).
GitHub Actions supports [sharding tests between multiple jobs](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs). Check out our [sharding doc](./test-sharding) to learn more about sharding and to see a [GitHub actions example](./test-sharding.md#github-actions-example) of how to configure a job to run your tests on multiple machines as well as how to merge the HTML reports.
Looking at the list of steps in `jobs.test.steps`, you can see that the workflow performs these steps:
### Via Containers
GitHub Actions support [running jobs in a container](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container) by using the [`jobs.<job_id>.container`](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainer) option. This is useful to not pollute the host environment with dependencies and to have a consistent environment for e.g. screenshots/visual regression testing across different operating systems.
```yml js title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
playwright:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v%%VERSION%%-jammy
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Run your tests
run: npx playwright test
env:
HOME: /root
```
```yml python title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
playwright:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright/python:v%%VERSION%%-jammy
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r local-requirements.txt
pip install -e .
- name: Run your tests
run: pytest
env:
HOME: /root
```
```yml java title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
playwright:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright/java:v%%VERSION%%-jammy
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test
env:
HOME: /root
```
```yml csharp title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
playwright:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright/dotnet:v%%VERSION%%-jammy
steps:
- uses: actions/checkout@v4
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- run: dotnet build
- name: Run your tests
run: dotnet test
env:
HOME: /root
```
### On deployment
This will start the tests after a [GitHub Deployment](https://developer.github.com/v3/repos/deployments/) went into the `success` state.
Services like Vercel use this pattern so you can run your end-to-end tests on their deployed environment.
```yml js title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
deployment_status:
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
if: github.event.deployment_status.state == 'success'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
env:
PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}
```
```yml python title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
deployment_status:
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
if: github.event.deployment_status.state == 'success'
steps:
- uses: actions/checkout@v4
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Ensure browsers are installed
run: python -m playwright install --with-deps
- name: Run tests
run: pytest
env:
# This might depend on your test-runner
PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}
```
```yml java title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
deployment_status:
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
if: github.event.deployment_status.state == 'success'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Install Playwright
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps"
- name: Run tests
run: mvn test
env:
# This might depend on your test-runner
PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}
```
```yml csharp title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
deployment_status:
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
if: github.event.deployment_status.state == 'success'
steps:
- uses: actions/checkout@v4
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- run: dotnet build
- name: Ensure browsers are installed
run: pwsh bin/Debug/net8.0/playwright.ps1 install --with-deps
- name: Run tests
run: dotnet test
env:
# This might depend on your test-runner
PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}
```
### Fail-Fast
* langs: js
Even with sharding enabled, large test suites can take very long to execute. Running changed test files first on PRs will give you a faster feedback loop and use less CI resources.
```yml js title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run changed Playwright tests
run: npx playwright test --only-changed=$GITHUB_BASE_REF
if: github.event_name == 'pull_request'
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
```
1. Clone your repository
2. Install language dependencies
3. Install project dependencies and build
4. Install Playwright Browsers
5. Run tests
## Create a Repo and Push to GitHub
@ -569,4 +322,5 @@ This step will not work for pull requests created from a forked repository becau
- [Learn how to perform Actions](./input.md)
- [Learn how to write Assertions](./test-assertions.md)
- [Learn more about the Trace Viewer](/trace-viewer.md)
- [Learn more about running tests on other CI providers](/ci.md)
- [Learn more ways of running tests on GitHub Actions](/ci.md)
- [Learn more about running tests on other CI providers](/ci.md#github-actions) // TODO: is this link correct?

View file

@ -59,14 +59,398 @@ export default defineConfig({
});
```
## CI configurations
The [Command line tools](./browsers#install-system-dependencies) can be used to install all operating system dependencies on GitHub Actions.
The [Command line tools](./browsers#install-system-dependencies) can be used to install all operating system dependencies in CI.
### GitHub Actions
Check out our [GitHub Actions](ci-intro.md) guide for more information on how to run your tests on GitHub.
#### On push/pull_request
* langs: js
Tests will run on push or pull request on branches main/master. The [workflow](https://docs.github.com/en/actions/using-workflows/about-workflows) will install all dependencies, install Playwright and then run the tests. It will also create the HTML report.
```yml js title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
```
#### On push/pull_request
* langs: python, java, csharp
Tests will run on push or pull request on branches main/master. The [workflow](https://docs.github.com/en/actions/using-workflows/about-workflows) will install all dependencies, install Playwright and then run the tests.
```yml python title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Ensure browsers are installed
run: python -m playwright install --with-deps
- name: Run your tests
run: pytest --tracing=retain-on-failure
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-traces
path: test-results/
```
```yml java title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Ensure browsers are installed
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps"
- name: Run tests
run: mvn test
```
```yml csharp title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Build & Install
run: dotnet build
- name: Ensure browsers are installed
run: pwsh bin/Debug/net8.0/playwright.ps1 install --with-deps
- name: Run your tests
run: dotnet test
```
#### On push/pull_request (sharded)
* langs: js
GitHub Actions supports [sharding tests between multiple jobs](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs). Check out our [sharding doc](./test-sharding) to learn more about sharding and to see a [GitHub actions example](./test-sharding.md#github-actions-example) of how to configure a job to run your tests on multiple machines as well as how to merge the HTML reports.
#### Via Containers
GitHub Actions support [running jobs in a container](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container) by using the [`jobs.<job_id>.container`](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainer) option. This is useful to not pollute the host environment with dependencies and to have a consistent environment for e.g. screenshots/visual regression testing across different operating systems.
```yml js title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
playwright:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v%%VERSION%%-jammy
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Run your tests
run: npx playwright test
env:
HOME: /root
```
```yml python title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
playwright:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright/python:v%%VERSION%%-jammy
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r local-requirements.txt
pip install -e .
- name: Run your tests
run: pytest
env:
HOME: /root
```
```yml java title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
playwright:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright/java:v%%VERSION%%-jammy
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Run tests
run: mvn test
env:
HOME: /root
```
```yml csharp title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
playwright:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright/dotnet:v%%VERSION%%-jammy
steps:
- uses: actions/checkout@v4
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- run: dotnet build
- name: Run your tests
run: dotnet test
env:
HOME: /root
```
#### On deployment
This will start the tests after a [GitHub Deployment](https://developer.github.com/v3/repos/deployments/) went into the `success` state.
Services like Vercel use this pattern so you can run your end-to-end tests on their deployed environment.
```yml js title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
deployment_status:
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
if: github.event.deployment_status.state == 'success'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
env:
PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}
```
```yml python title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
deployment_status:
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
if: github.event.deployment_status.state == 'success'
steps:
- uses: actions/checkout@v4
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Ensure browsers are installed
run: python -m playwright install --with-deps
- name: Run tests
run: pytest
env:
# This might depend on your test-runner
PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}
```
```yml java title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
deployment_status:
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
if: github.event.deployment_status.state == 'success'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Build & Install
run: mvn -B install -D skipTests --no-transfer-progress
- name: Install Playwright
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps"
- name: Run tests
run: mvn test
env:
# This might depend on your test-runner
PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}
```
```yml csharp title=".github/workflows/playwright.yml"
name: Playwright Tests
on:
deployment_status:
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
if: github.event.deployment_status.state == 'success'
steps:
- uses: actions/checkout@v4
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- run: dotnet build
- name: Ensure browsers are installed
run: pwsh bin/Debug/net8.0/playwright.ps1 install --with-deps
- name: Run tests
run: dotnet test
env:
# This might depend on your test-runner
PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}
```
#### Fail-Fast
* langs: js
Large test suites can take very long to execute. By executing a preliminary test run with the `--only-changed` flag, you can run test files that are likely to fail first.
This will give you a faster feedback loop and slightly lower CI consumption while working on Pull Requests.
To detect test files affected by your changeset, `--only-changed` analyses your suites' dependency graph. This is a heuristic and might miss tests, so it's important that you always run the full test suite after the preliminary test run.
```yml js title=".github/workflows/playwright.yml" {20-23}
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run changed Playwright tests
run: npx playwright test --only-changed=$GITHUB_BASE_REF
if: github.event_name == 'pull_request'
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
```
### Docker
@ -660,4 +1044,4 @@ xvfb-run mvn test
```
```bash csharp
xvfb-run dotnet test
```
```

View file

@ -68,9 +68,95 @@ int status = await page.EvaluateAsync<int>(@"async () => {
}");
```
## Different environments
Evaluated scripts run in the browser environment, while your test runs in a testing environments. This means you cannot use variables from your test in the page and vice versa. Instead, you should pass them explicitly as an argument.
The following snippet is **WRONG** because it uses the variable directly:
```js
const data = 'some data';
const result = await page.evaluate(() => {
// WRONG: there is no "data" in the web page.
window.myApp.use(data);
});
```
```java
String data = "some data";
Object result = page.evaluate("() => {\n" +
" // WRONG: there is no 'data' in the web page.\n" +
" window.myApp.use(data);\n" +
"}");
```
```python async
data = "some data"
result = await page.evaluate("""() => {
// WRONG: there is no "data" in the web page.
window.myApp.use(data)
}""")
```
```python sync
data = "some data"
result = page.evaluate("""() => {
// WRONG: there is no "data" in the web page.
window.myApp.use(data)
}""")
```
```csharp
var data = "some data";
var result = await page.EvaluateAsync(@"() => {
// WRONG: there is no 'data' in the web page.
window.myApp.use(data);
}");
```
The following snippet is **CORRECT** because it passes the value explicitly as an argument:
```js
const data = 'some data';
// Pass |data| as a parameter.
const result = await page.evaluate(data => {
window.myApp.use(data);
}, data);
```
```java
String data = "some data";
// Pass |data| as a parameter.
Object result = page.evaluate("data => {\n" +
" window.myApp.use(data);\n" +
"}", data);
```
```python async
data = "some data"
# Pass |data| as a parameter.
result = await page.evaluate("""data => {
window.myApp.use(data)
}""", data)
```
```python sync
data = "some data"
# Pass |data| as a parameter.
result = page.evaluate("""data => {
window.myApp.use(data)
}""", data)
```
```csharp
var data = "some data";
// Pass |data| as a parameter.
var result = await page.EvaluateAsync("data => { window.myApp.use(data); }", data);
```
## Evaluation Argument
Playwright evaluation methods like [`method: Page.evaluate`] take a single optional argument. This argument can be a mix of [Serializable] values and [JSHandle] or [ElementHandle] instances. Handles are automatically converted to the value they represent.
Playwright evaluation methods like [`method: Page.evaluate`] take a single optional argument. This argument can be a mix of [Serializable] values and [JSHandle] instances. Handles are automatically converted to the value they represent.
```js
// A primitive value.
@ -86,7 +172,7 @@ await page.evaluate(object => object.foo, { foo: 'bar' });
const button = await page.evaluateHandle('window.button');
await page.evaluate(button => button.textContent, button);
// Alternative notation using elementHandle.evaluate.
// Alternative notation using JSHandle.evaluate.
await button.evaluate((button, from) => button.textContent.substring(from), 5);
// Object with multiple handles.
@ -109,7 +195,7 @@ await page.evaluate(
([b1, b2]) => b1.textContent + b2.textContent,
[button1, button2]);
// Any non-cyclic mix of serializables and handles works.
// Any mix of serializables and handles works.
await page.evaluate(
x => x.button1.textContent + x.list[0].textContent + String(x.foo),
{ button1, list: [button2], foo: null });
@ -131,7 +217,7 @@ page.evaluate("object => object.foo", obj);
ElementHandle button = page.evaluateHandle("window.button");
page.evaluate("button => button.textContent", button);
// Alternative notation using elementHandle.evaluate.
// Alternative notation using JSHandle.evaluate.
button.evaluate("(button, from) => button.textContent.substring(from)", 5);
// Object with multiple handles.
@ -156,7 +242,7 @@ page.evaluate(
"([b1, b2]) => b1.textContent + b2.textContent",
Arrays.asList(button1, button2));
// Any non-cyclic mix of serializables and handles works.
// Any mix of serializables and handles works.
Map<String, Object> arg = new HashMap<>();
arg.put("button1", button1);
arg.put("list", Arrays.asList(button2));
@ -180,7 +266,7 @@ await page.evaluate('object => object.foo', { 'foo': 'bar' })
button = await page.evaluate_handle('button')
await page.evaluate('button => button.textContent', button)
# Alternative notation using elementHandle.evaluate.
# Alternative notation using JSHandle.evaluate.
await button.evaluate('(button, from) => button.textContent.substring(from)', 5)
# Object with multiple handles.
@ -203,7 +289,7 @@ await page.evaluate("""
([b1, b2]) => b1.textContent + b2.textContent""",
[button1, button2])
# Any non-cyclic mix of serializables and handles works.
# Any mix of serializables and handles works.
await page.evaluate("""
x => x.button1.textContent + x.list[0].textContent + String(x.foo)""",
{ 'button1': button1, 'list': [button2], 'foo': None })
@ -223,7 +309,7 @@ page.evaluate('object => object.foo', { 'foo': 'bar' })
button = page.evaluate_handle('window.button')
page.evaluate('button => button.textContent', button)
# Alternative notation using elementHandle.evaluate.
# Alternative notation using JSHandle.evaluate.
button.evaluate('(button, from) => button.textContent.substring(from)', 5)
# Object with multiple handles.
@ -245,7 +331,7 @@ page.evaluate("""
([b1, b2]) => b1.textContent + b2.textContent""",
[button1, button2])
# Any non-cyclic mix of serializables and handles works.
# Any mix of serializables and handles works.
page.evaluate("""
x => x.button1.textContent + x.list[0].textContent + String(x.foo)""",
{ 'button1': button1, 'list': [button2], 'foo': None })
@ -265,7 +351,7 @@ await page.EvaluateAsync<object>("object => object.foo", new { foo = "bar" });
var button = await page.EvaluateHandleAsync("window.button");
await page.EvaluateAsync<IJSHandle>("button => button.textContent", button);
// Alternative notation using elementHandle.EvaluateAsync.
// Alternative notation using JSHandle.EvaluateAsync.
await button.EvaluateAsync<string>("(button, from) => button.textContent.substring(from)", 5);
// Object with multiple handles.
@ -282,93 +368,69 @@ await page.EvaluateAsync("({ button1, button2 }) => button1.textContent + button
// Note the required parenthesis.
await page.EvaluateAsync("([b1, b2]) => b1.textContent + b2.textContent", new[] { button1, button2 });
// Any non-cyclic mix of serializables and handles works.
// Any mix of serializables and handles works.
await page.EvaluateAsync("x => x.button1.textContent + x.list[0].textContent + String(x.foo)", new { button1, list = new[] { button2 }, foo = null as object });
```
Right:
## Init scripts
Sometimes it is convenient to evaluate something in the page before it starts loading. For example, you might want to setup some mocks or test data.
In this case, use [`method: Page.addInitScript`] or [`method: BrowserContext.addInitScript`]. In the example below, we will replace `Math.random()` with a constant value.
First, create a `preload.js` file that contains the mock.
```js browser
// preload.js
Math.random = () => 42;
```
Next, add init script to the page.
```js
const data = { text: 'some data', value: 1 };
// Pass |data| as a parameter.
const result = await page.evaluate(data => {
window.myApp.use(data);
}, data);
```
import { test, expect } from '@playwright/test';
import path from 'path';
```java
Map<String, Object> data = new HashMap<>();
data.put("text", "some data");
data.put("value", 1);
// Pass |data| as a parameter.
Object result = page.evaluate("data => {\n" +
" window.myApp.use(data);\n" +
"}", data);
```
```python async
data = { 'text': 'some data', 'value': 1 }
# Pass |data| as a parameter.
result = await page.evaluate("""data => {
window.myApp.use(data)
}""", data)
```
```python sync
data = { 'text': 'some data', 'value': 1 }
# Pass |data| as a parameter.
result = page.evaluate("""data => {
window.myApp.use(data)
}""", data)
```
```csharp
var data = new { text = "some data", value = 1};
// Pass data as a parameter
var result = await page.EvaluateAsync("data => { window.myApp.use(data); }", data);
```
Wrong:
```js
const data = { text: 'some data', value: 1 };
const result = await page.evaluate(() => {
// There is no |data| in the web page.
window.myApp.use(data);
test.beforeEach(async ({ page }) => {
// Add script for every test in the beforeEach hook.
// Make sure to correctly resolve the script path.
await page.addInitScript({ path: path.resolve(__dirname, '../mocks/preload.js') });
});
```
```java
Map<String, Object> data = new HashMap<>();
data.put("text", "some data");
data.put("value", 1);
Object result = page.evaluate("() => {\n" +
" // There is no |data| in the web page.\n" +
" window.myApp.use(data);\n" +
"}");
// In your test, assuming the "preload.js" file is in the "mocks" directory.
page.addInitScript(Paths.get("mocks/preload.js"));
```
```python async
data = { 'text': 'some data', 'value': 1 }
result = await page.evaluate("""() => {
// There is no |data| in the web page.
window.myApp.use(data)
}""")
# In your test, assuming the "preload.js" file is in the "mocks" directory.
await page.add_init_script(path="mocks/preload.js")
```
```python sync
data = { 'text': 'some data', 'value': 1 }
result = page.evaluate("""() => {
// There is no |data| in the web page.
window.myApp.use(data)
}""")
# In your test, assuming the "preload.js" file is in the "mocks" directory.
page.add_init_script(path="mocks/preload.js")
```
```csharp
var data = new { text = "some data", value = 1};
// Pass data as a parameter
var result = await page.EvaluateAsync(@"data => {
// There is no |data| in the web page.
window.myApp.use(data);
}");
// In your test, assuming the "preload.js" file is in the "mocks" directory.
await Page.AddInitScriptAsync(scriptPath: "mocks/preload.js");
```
######
* langs: js
Alternatively, you can pass a function instead of creating a preload script file. This is more convenient for short or one-off scripts. You can also pass an argument this way.
```js
import { test, expect } from '@playwright/test';
// Add script for every test in the beforeEach hook.
test.beforeEach(async ({ page }) => {
const value = 42;
await page.addInitScript(value => {
Math.random = () => value;
}, value);
});
```

861
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -85,8 +85,8 @@
"eslint": "^8.55.0",
"eslint-plugin-internal-playwright": "file:utils/eslint-plugin-internal-playwright",
"eslint-plugin-notice": "^0.9.10",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"formidable": "^2.1.1",
"license-checker": "^25.0.1",
"mime": "^3.0.0",

View file

@ -70,19 +70,19 @@ export const blank = () => {
};
export const externalLink = () => {
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fill-rule='evenodd' d='M10.604 1h4.146a.25.25 0 01.25.25v4.146a.25.25 0 01-.427.177L13.03 4.03 9.28 7.78a.75.75 0 01-1.06-1.06l3.75-3.75-1.543-1.543A.25.25 0 0110.604 1zM3.75 2A1.75 1.75 0 002 3.75v8.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 12.25v-3.5a.75.75 0 00-1.5 0v3.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-8.5a.25.25 0 01.25-.25h3.5a.75.75 0 000-1.5h-3.5z'></path></svg>;
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fillRule='evenodd' d='M10.604 1h4.146a.25.25 0 01.25.25v4.146a.25.25 0 01-.427.177L13.03 4.03 9.28 7.78a.75.75 0 01-1.06-1.06l3.75-3.75-1.543-1.543A.25.25 0 0110.604 1zM3.75 2A1.75 1.75 0 002 3.75v8.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 12.25v-3.5a.75.75 0 00-1.5 0v3.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-8.5a.25.25 0 01.25-.25h3.5a.75.75 0 000-1.5h-3.5z'></path></svg>;
};
export const calendar = () => {
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fill-rule='evenodd' d='M4.75 0a.75.75 0 01.75.75V2h5V.75a.75.75 0 011.5 0V2h1.25c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0113.25 16H2.75A1.75 1.75 0 011 14.25V3.75C1 2.784 1.784 2 2.75 2H4V.75A.75.75 0 014.75 0zm0 3.5h8.5a.25.25 0 01.25.25V6h-11V3.75a.25.25 0 01.25-.25h2zm-2.25 4v6.75c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25V7.5h-11z'></path></svg>;
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fillRule='evenodd' d='M4.75 0a.75.75 0 01.75.75V2h5V.75a.75.75 0 011.5 0V2h1.25c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0113.25 16H2.75A1.75 1.75 0 011 14.25V3.75C1 2.784 1.784 2 2.75 2H4V.75A.75.75 0 014.75 0zm0 3.5h8.5a.25.25 0 01.25.25V6h-11V3.75a.25.25 0 01.25-.25h2zm-2.25 4v6.75c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25V7.5h-11z'></path></svg>;
};
export const person = () => {
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fill-rule='evenodd' d='M10.5 5a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm.061 3.073a4 4 0 10-5.123 0 6.004 6.004 0 00-3.431 5.142.75.75 0 001.498.07 4.5 4.5 0 018.99 0 .75.75 0 101.498-.07 6.005 6.005 0 00-3.432-5.142z'></path></svg>;
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fillRule='evenodd' d='M10.5 5a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm.061 3.073a4 4 0 10-5.123 0 6.004 6.004 0 00-3.431 5.142.75.75 0 001.498.07 4.5 4.5 0 018.99 0 .75.75 0 101.498-.07 6.005 6.005 0 00-3.432-5.142z'></path></svg>;
};
export const commit = () => {
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fill-rule='evenodd' d='M10.5 7.75a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm1.43.75a4.002 4.002 0 01-7.86 0H.75a.75.75 0 110-1.5h3.32a4.001 4.001 0 017.86 0h3.32a.75.75 0 110 1.5h-3.32z'></path></svg>;
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fillRule='evenodd' d='M10.5 7.75a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm1.43.75a4.002 4.002 0 01-7.86 0H.75a.75.75 0 110-1.5h3.32a4.001 4.001 0 017.86 0h3.32a.75.75 0 110 1.5h-3.32z'></path></svg>;
};
export const image = () => {

View file

@ -81,7 +81,7 @@ export const AttachmentLink: React.FunctionComponent<{
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
{!attachment.path && <span>{linkifyText(attachment.name)}</span>}
</span>} loadChildren={attachment.body ? () => {
return [<div className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
} : undefined} depth={0} style={{ lineHeight: '32px' }}></TreeItem>;
};

View file

@ -58,7 +58,7 @@ export const TestCaseView: React.FC<{
{labels && <LabelsLinkView labels={labels} />}
</div>}
{!!visibleAnnotations.length && <AutoChip header='Annotations'>
{visibleAnnotations.map(annotation => <TestCaseAnnotationView annotation={annotation} />)}
{visibleAnnotations.map((annotation, index) => <TestCaseAnnotationView key={index} annotation={annotation} />)}
</AutoChip>}
{test && <TabbedPane tabs={
test.results.map((result, index) => ({

View file

@ -1,11 +0,0 @@
# Certfificates for Socks Proxy
These certificates are used when client certificates are used with
Playwright. Playwright then creates a Socks proxy, which sits between
the browser and the actual target server. The Socks proxy uses this certificiate
to talk to the browser and establishes its own secure TLS connection to the server.
The certificates are generated via:
```bash
openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 -keyout key.pem -out cert.pem -subj "/CN=localhost"
```

View file

@ -1,19 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUTcrzEueVL/OuLHr4LBIPWeS4UL0wDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDcwNDA4NDAzNFoXDTM0MDcw
MjA4NDAzNFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEApof+SZVN4UGma4xJDVHhMSpmEJoCdMPr+HFadJJK/brF
BNOhA1C5wNk8oD/XYo7enAHQH/EsBnq4MMxv79rXTGnIdXMF+43GdMDh5kh81FQy
Esw8Vt4eif9eZkjUxI2GHhR2ovJewmQa7E+SeUB2RzJTqz8QPLhd74JFfgaci+S2
8L37ScVjcw55T1PcNflzB4vwsQHBT3yND0MLDhm+8MLzmTl4Mw5PgIOaBl5Jh8Tr
wQF4eeeB3FPJoMQhTP8aGBjW1mo+NmSSRAPIAZyhmCAnDeC33yRjAaiHjaL5Pr9f
wt5zoF5+U1xWhGXWzGOE6p/VTj62F9a2fOXNHclYJQIDAQABo1MwUTAdBgNVHQ4E
FgQU9BoVzGtb5x70KqGO/89N1hyqi5kwHwYDVR0jBBgwFoAU9BoVzGtb5x70KqGO
/89N1hyqi5kwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAYcbI
wvcfx2p8z0RNN3EA+epKX1SagZyJX4ORIO8kln1sDU+ceHde3n3xnp1dg6HG2qh1
a7CZub/fNUaP9R8+6iiV0wPT7Ybkb2NIJcH1yq+/bfSS5OC5DO0yv9SUADdBoDwa
zOuBAqdcYW1BHYcbAzsQnniRcejHu06ioaS6SwwJ8150rQnLT4Lh9LAl40W6v4nZ
NdTGQETTrbjcgH1ER4IhWTKtVyPOxGF9A/OOawMEdfS8BhUO7YRS4QNFFaQMrJAb
MDhDtjSyDogLr8P43xjjWvQWG9a7zTF0kKEsdJ0cEG5HATpg8bPHmrouxbs2HGeH
kJXzMykrsYyXsInN3w==
-----END CERTIFICATE-----

View file

@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCmh/5JlU3hQaZr
jEkNUeExKmYQmgJ0w+v4cVp0kkr9usUE06EDULnA2TygP9dijt6cAdAf8SwGergw
zG/v2tdMach1cwX7jcZ0wOHmSHzUVDISzDxW3h6J/15mSNTEjYYeFHai8l7CZBrs
T5J5QHZHMlOrPxA8uF3vgkV+BpyL5LbwvftJxWNzDnlPU9w1+XMHi/CxAcFPfI0P
QwsOGb7wwvOZOXgzDk+Ag5oGXkmHxOvBAXh554HcU8mgxCFM/xoYGNbWaj42ZJJE
A8gBnKGYICcN4LffJGMBqIeNovk+v1/C3nOgXn5TXFaEZdbMY4Tqn9VOPrYX1rZ8
5c0dyVglAgMBAAECggEAB6zX4vNPKhUZAvbtvP/rlZUDLDu05kXLX+F1jk7ZxvTv
NKg+UQVM8l7wxN/8YM3944nP2lEGuuu4BoO9mvvmlV6Avy0EdxITNflX0AHCQxT4
U9Z253gIR0ruQl+T8tUk+8jsqNjr1iC//ukx8oWujdx7b7aR3IKQzcOeyU6rs2TN
lyrVVsEaFVi9+wCw0xyiCmPlobrn+egdigw7Zhp2BRinC6W9eMxuPS2hlhQUhBm/
eiD96YWp0RAv/L5qO93reoXIAzrrLdcUgPEnnq1zN7y2xihU2+B2sTph1m/A26+J
yPcXd7vQrXlRXQU6PaCa+0oJULlpiAzy3HPbnr4BkQKBgQDdmekTX8dQqiEZPX1C
017QRFbx0/x/TDFDSeJbDeauMzzCaGqCO2WVmYmTvFtby2G4/6BYowVtJVHm4uJl
XsYk8dWIQGLPIj1Cw7ZieJvb2EVRxgnY2oMaOTOazHzPHFzZV718zwEeZrryT82J
881E8wgM8V3DjkS4ye3TbwvimQKBgQDAYa/IdnpAg5z1TREi9Tt8fnoGpmSscAak
USgeXVsvoNzXXkE94MiiCOOrX1r68TWYDAzq6MKGDewkWOfLwXWR6D5C2LyE1q9P
1pxstgs/nC3ZUTz0yEH47ahSmhywhGlvXXOQEXUSLiVTOdeMCubMqwQW80F1868n
aBHcj5/lbQKBgQDIojjsWaNT3TTqbUmj30vQtI8jlBLgDlPr4FEYr5VT0wAH5BHK
p4xpzgFJyRfOHG312TuMBM087LUinfjsXsp3WJ1EJ0dO0mk0sY3HyfsTKNRaHTt9
Ixnf/DpExS+bNMq73Tyqa6FPrSNFkAtAA4SuEHwRe9aw33ZI+EpjS/8uwQKBgQCi
9NwqSLlLVnColEw0uVdXH+cLJPzX19i4bQo3lkp8MJ2ATJWk7XflUPRQoGf3ckQ8
c9CpVtoXJUnmi+xkeo21Nu0uQFqHhzZewWIk75rdmdR4ZUjl649+ZQkUVviASNjq
fVU7Lp5k9POm6LL9K+rOaPoA2rKTUAQItC2VD4+YjQKBgB6kgvgN6Mz/u0RE3kkV
2GOoP5sso71Hxwh7o6JEzUMhR+e/T/LLcBwEjLYcf1FYRySHsXLn2Ar/Uw1J7pAZ
ud54/at+7mTDliaT8Ar7S9vcso7ZfmuDX9qB9+c77idPskVBPo2tjJbwvFcB6sww
5Elcfmj6tEP4YLJ6Kv3qTPhT
-----END PRIVATE KEY-----

View file

@ -9,9 +9,9 @@
},
{
"name": "chromium-tip-of-tree",
"revision": "1249",
"revision": "1250",
"installByDefault": false,
"browserVersion": "129.0.6654.0"
"browserVersion": "129.0.6658.0"
},
{
"name": "firefox",
@ -27,7 +27,7 @@
},
{
"name": "webkit",
"revision": "2061",
"revision": "2062",
"installByDefault": true,
"revisionOverrides": {
"mac10.14": "1446",

View file

@ -308,7 +308,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
}
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void> {
const source = await evaluationScript(script, arg);
const source = await evaluationScript(script, arg, arguments.length > 1);
await this._channel.addInitScript({ source });
}
@ -552,13 +552,19 @@ function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
export async function toClientCertificatesProtocol(certs?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
if (!certs)
return undefined;
return await Promise.all(certs.map(async cert => {
return {
origin: cert.origin,
cert: cert.certPath ? await fs.promises.readFile(cert.certPath) : undefined,
key: cert.keyPath ? await fs.promises.readFile(cert.keyPath) : undefined,
pfx: cert.pfxPath ? await fs.promises.readFile(cert.pfxPath) : undefined,
passphrase: cert.passphrase,
};
}));
const bufferizeContent = async (value?: Buffer, path?: string): Promise<Buffer | undefined> => {
if (value)
return value;
if (path)
return await fs.promises.readFile(path);
};
return await Promise.all(certs.map(async cert => ({
origin: cert.origin,
cert: await bufferizeContent(cert.cert, cert.certPath),
key: await bufferizeContent(cert.key, cert.keyPath),
pfx: await bufferizeContent(cert.pfx, cert.pfxPath),
passphrase: cert.passphrase,
})));
}

View file

@ -28,20 +28,37 @@ export function envObjectToArray(env: types.Env): { name: string, value: string
return result;
}
export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise<string> {
export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg: any, hasArg: boolean, addSourceUrl: boolean = true): Promise<string> {
if (typeof fun === 'function') {
const source = fun.toString();
const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg);
return `(${source})(${argString})`;
}
if (arg !== undefined)
throw new Error('Cannot evaluate a string with arguments');
if (isString(fun))
if (isString(fun)) {
if (arg !== undefined)
throw new Error('Cannot evaluate a string with arguments');
return fun;
if (fun.content !== undefined)
}
if (fun.content !== undefined) {
if (arg !== undefined)
throw new Error('Cannot evaluate a string with arguments');
return fun.content;
}
if (fun.path !== undefined) {
let source = await fs.promises.readFile(fun.path, 'utf8');
if (hasArg) {
// Assume a CJS module that has a function default export.
source = `(() => {
var exports = {}; var module = { exports };
${source}
let __pw_result__ = module.exports;
if (__pw_result__ && typeof __pw_result__ === 'object' && ('default' in __pw_result__))
__pw_result__ = __pw_result__['default'];
if (typeof __pw_result__ !== 'function')
return __pw_result__;
return __pw_result__(${JSON.stringify(arg)});
})()`;
}
if (addSourceUrl)
source = addSourceUrlToScript(source, fun.path);
return source;

View file

@ -492,7 +492,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
const source = await evaluationScript(script, arg);
const source = await evaluationScript(script, arg, arguments.length > 1);
await this._channel.addInitScript({ source });
}

View file

@ -26,7 +26,7 @@ export class Selectors implements api.Selectors {
private _registrations: channels.SelectorsRegisterParams[] = [];
async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> {
const source = await evaluationScript(script, undefined, false);
const source = await evaluationScript(script, undefined, false, false);
const params = { ...options, name, source };
for (const channel of this._channels)
await channel._channel.register(params);

View file

@ -49,8 +49,11 @@ export const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domconten
export type ClientCertificate = {
origin: string;
cert?: Buffer;
certPath?: string;
key?: Buffer;
keyPath?: string;
pfx?: Buffer;
pfxPath?: string;
passphrase?: string;
};

View file

@ -40,7 +40,7 @@ import { Tracing } from './trace/recorder/tracing';
import type * as types from './types';
import type { HeadersArray, ProxySettings } from './types';
import { kMaxCookieExpiresDateInSeconds } from './network';
import { clientCertificatesToTLSOptions, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
type FetchRequestOptions = {
userAgent: string;
@ -195,7 +195,7 @@ export abstract class APIRequestContext extends SdkObject {
maxRedirects: params.maxRedirects === 0 ? -1 : params.maxRedirects === undefined ? 20 : params.maxRedirects,
timeout,
deadline,
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, requestUrl.origin),
...getMatchingTLSOptionsForOrigin(this._defaultOptions().clientCertificates, requestUrl.origin),
__testHookLookup: (params as any).__testHookLookup,
};
// rejectUnauthorized = undefined is treated as true in Node.js 12.
@ -365,7 +365,7 @@ export abstract class APIRequestContext extends SdkObject {
maxRedirects: options.maxRedirects - 1,
timeout: options.timeout,
deadline: options.deadline,
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, url.origin),
...getMatchingTLSOptionsForOrigin(this._defaultOptions().clientCertificates, url.origin),
__testHookLookup: options.__testHookLookup,
};
// rejectUnauthorized = undefined is treated as true in node 12.

View file

@ -659,18 +659,24 @@ export class Frame extends SdkObject {
}
url = helper.completeUserURL(url);
const sameDocument = helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (e: NavigationEvent) => !e.newDocument);
const navigateResult = await this._page._delegate.navigateFrame(this, url, referer);
const navigationEvents: NavigationEvent[] = [];
const collectNavigations = (arg: NavigationEvent) => navigationEvents.push(arg);
this.on(Frame.Events.InternalNavigation, collectNavigations);
const navigateResult = await this._page._delegate.navigateFrame(this, url, referer).finally(
() => this.off(Frame.Events.InternalNavigation, collectNavigations));
let event: NavigationEvent;
if (navigateResult.newDocumentId) {
sameDocument.dispose();
event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (event: NavigationEvent) => {
const predicate = (event: NavigationEvent) => {
// We are interested either in this specific document, or any other document that
// did commit and replaced the expected document.
return event.newDocument && (event.newDocument.documentId === navigateResult.newDocumentId || !event.error);
}).promise;
};
const events = navigationEvents.filter(predicate);
if (events.length)
event = events[0];
else
event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, predicate).promise;
if (event.newDocument!.documentId !== navigateResult.newDocumentId) {
// This is just a sanity check. In practice, new navigation should
// cancel the previous one and report "request cancelled"-like error.
@ -679,7 +685,13 @@ export class Frame extends SdkObject {
if (event.error)
throw event.error;
} else {
event = await sameDocument.promise;
// Wait for same document navigation.
const predicate = (e: NavigationEvent) => !e.newDocument;
const events = navigationEvents.filter(predicate);
if (events.length)
event = events[0];
else
event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, predicate).promise;
}
if (!this._firedLifecycleEvents.has(waitUntil))

View file

@ -552,28 +552,14 @@ class TextAssertionTool implements RecorderTool {
private _recorder: Recorder;
private _hoverHighlight: HighlightModel | null = null;
private _action: actions.AssertAction | null = null;
private _dialogElement: HTMLElement | null = null;
private _acceptButton: HTMLElement;
private _cancelButton: HTMLElement;
private _keyboardListener: ((event: KeyboardEvent) => void) | undefined;
private _dialog: Dialog;
private _textCache = new Map<Element | ShadowRoot, ElementText>();
private _kind: 'text' | 'value';
constructor(recorder: Recorder, kind: 'text' | 'value') {
this._recorder = recorder;
this._kind = kind;
this._acceptButton = this._recorder.document.createElement('x-pw-tool-item');
this._acceptButton.title = 'Accept';
this._acceptButton.classList.add('accept');
this._acceptButton.appendChild(this._recorder.document.createElement('x-div'));
this._acceptButton.addEventListener('click', () => this._commit());
this._cancelButton = this._recorder.document.createElement('x-pw-tool-item');
this._cancelButton.title = 'Close';
this._cancelButton.classList.add('cancel');
this._cancelButton.appendChild(this._recorder.document.createElement('x-div'));
this._cancelButton.addEventListener('click', () => this._closeDialog());
this._dialog = new Dialog(recorder);
}
cursor() {
@ -581,7 +567,7 @@ class TextAssertionTool implements RecorderTool {
}
cleanup() {
this._closeDialog();
this._dialog.close();
this._hoverHighlight = null;
}
@ -590,7 +576,7 @@ class TextAssertionTool implements RecorderTool {
if (this._kind === 'value') {
this._commitAssertValue();
} else {
if (!this._dialogElement)
if (!this._dialog.isShowing())
this._showDialog();
}
}
@ -611,7 +597,7 @@ class TextAssertionTool implements RecorderTool {
}
onMouseMove(event: MouseEvent) {
if (this._dialogElement)
if (this._dialog.isShowing())
return;
const target = this._recorder.deepEventTarget(event);
if (this._hoverHighlight?.elements[0] === target)
@ -691,9 +677,9 @@ class TextAssertionTool implements RecorderTool {
}
private _commit() {
if (!this._action || !this._dialogElement)
if (!this._action || !this._dialog.isShowing())
return;
this._closeDialog();
this._dialog.close();
this._recorder.delegate.recordAction?.(this._action);
this._recorder.delegate.setMode?.('recording');
}
@ -705,31 +691,6 @@ class TextAssertionTool implements RecorderTool {
if (!this._action || this._action.name !== 'assertText')
return;
this._dialogElement = this._recorder.document.createElement('x-pw-dialog');
this._keyboardListener = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
this._closeDialog();
return;
}
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
if (this._dialogElement)
this._commit();
return;
}
};
this._recorder.document.addEventListener('keydown', this._keyboardListener, true);
const toolbarElement = this._recorder.document.createElement('x-pw-tools-list');
const labelElement = this._recorder.document.createElement('label');
labelElement.textContent = 'Assert that element contains text';
toolbarElement.appendChild(labelElement);
toolbarElement.appendChild(this._recorder.document.createElement('x-spacer'));
toolbarElement.appendChild(this._acceptButton);
toolbarElement.appendChild(this._cancelButton);
this._dialogElement.appendChild(toolbarElement);
const bodyElement = this._recorder.document.createElement('x-pw-dialog-body');
const action = this._action;
const textElement = this._recorder.document.createElement('textarea');
textElement.setAttribute('spellcheck', 'false');
@ -747,24 +708,18 @@ class TextAssertionTool implements RecorderTool {
textElement.classList.toggle('does-not-match', !matches);
};
textElement.addEventListener('input', updateAndValidate);
bodyElement.appendChild(textElement);
this._dialogElement.appendChild(bodyElement);
this._recorder.highlight.appendChild(this._dialogElement);
const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, this._dialogElement);
this._dialogElement.style.top = position.anchorTop + 'px';
this._dialogElement.style.left = position.anchorLeft + 'px';
const label = 'Assert that element contains text';
const dialogElement = this._dialog.show({
label,
body: textElement,
onCommit: () => this._commit(),
});
const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, dialogElement);
this._dialog.moveTo(position.anchorTop, position.anchorLeft);
textElement.focus();
}
private _closeDialog() {
if (!this._dialogElement)
return;
this._dialogElement.remove();
this._recorder.document.removeEventListener('keydown', this._keyboardListener!);
this._dialogElement = null;
}
private _commitAssertValue() {
if (this._kind !== 'value')
return;
@ -1219,6 +1174,87 @@ export class Recorder {
}
}
class Dialog {
private _recorder: Recorder;
private _dialogElement: HTMLElement | null = null;
private _keyboardListener: ((event: KeyboardEvent) => void) | undefined;
constructor(recorder: Recorder) {
this._recorder = recorder;
}
isShowing(): boolean {
return !!this._dialogElement;
}
show(options: {
label: string;
body: Element;
onCommit: () => void;
onCancel?: () => void;
}) {
const acceptButton = this._recorder.document.createElement('x-pw-tool-item');
acceptButton.title = 'Accept';
acceptButton.classList.add('accept');
acceptButton.appendChild(this._recorder.document.createElement('x-div'));
acceptButton.addEventListener('click', () => options.onCommit());
const cancelButton = this._recorder.document.createElement('x-pw-tool-item');
cancelButton.title = 'Close';
cancelButton.classList.add('cancel');
cancelButton.appendChild(this._recorder.document.createElement('x-div'));
cancelButton.addEventListener('click', () => {
this.close();
options.onCancel?.();
});
this._dialogElement = this._recorder.document.createElement('x-pw-dialog');
this._keyboardListener = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
this.close();
options.onCancel?.();
return;
}
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
if (this._dialogElement)
options.onCommit();
return;
}
};
this._recorder.document.addEventListener('keydown', this._keyboardListener, true);
const toolbarElement = this._recorder.document.createElement('x-pw-tools-list');
const labelElement = this._recorder.document.createElement('label');
labelElement.textContent = options.label;
toolbarElement.appendChild(labelElement);
toolbarElement.appendChild(this._recorder.document.createElement('x-spacer'));
toolbarElement.appendChild(acceptButton);
toolbarElement.appendChild(cancelButton);
this._dialogElement.appendChild(toolbarElement);
const bodyElement = this._recorder.document.createElement('x-pw-dialog-body');
bodyElement.appendChild(options.body);
this._dialogElement.appendChild(bodyElement);
this._recorder.highlight.appendChild(this._dialogElement);
return this._dialogElement;
}
moveTo(top: number, left: number) {
if (!this._dialogElement)
return;
this._dialogElement.style.top = top + 'px';
this._dialogElement.style.left = left + 'px';
}
close() {
if (!this._dialogElement)
return;
this._dialogElement.remove();
this._recorder.document.removeEventListener('keydown', this._keyboardListener!);
this._dialogElement = null;
}
}
function deepActiveElement(document: Document): Element | null {
let activeElement = document.activeElement;
while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement)

View file

@ -261,12 +261,16 @@ function getAriaBoolean(attr: string | null) {
return attr === null ? undefined : attr.toLowerCase() === 'true';
}
function isElementIgnoredForAria(element: Element) {
return ['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element));
}
// https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion, but including "none" and "presentation" roles
// Not implemented:
// `Any descendants of elements that have the characteristic "Children Presentational: True"`
// https://www.w3.org/TR/wai-aria-1.2/#aria-hidden
export function isElementHiddenForAria(element: Element): boolean {
if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element)))
if (isElementIgnoredForAria(element))
return true;
const style = getElementComputedStyle(element);
const isSlot = element.nodeName === 'SLOT';
@ -371,7 +375,8 @@ function getPseudoContent(element: Element, pseudo: '::before' | '::after') {
}
function getPseudoContentImpl(pseudoStyle: CSSStyleDeclaration | undefined) {
if (!pseudoStyle)
// Note: all browsers ignore display:none and visibility:hidden pseudos.
if (!pseudoStyle || pseudoStyle.display === 'none' || pseudoStyle.visibility === 'hidden')
return '';
const content = pseudoStyle.content;
if ((content[0] === '\'' && content[content.length - 1] === '\'') ||
@ -496,14 +501,17 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
// step 2a. Hidden Not Referenced: If the current node is hidden and is:
// Not part of an aria-labelledby or aria-describedby traversal, where the node directly referenced by that relation was hidden.
// Nor part of a native host language text alternative element (e.g. label in HTML) or attribute traversal, where the root of that traversal was hidden.
if (!options.includeHidden &&
!options.embeddedInLabelledBy?.hidden &&
!options.embeddedInDescribedBy?.hidden &&
!options?.embeddedInNativeTextAlternative?.hidden &&
!options?.embeddedInLabel?.hidden &&
isElementHiddenForAria(element)) {
options.visitedElements.add(element);
return '';
if (!options.includeHidden) {
const isEmbeddedInHiddenReferenceTraversal =
!!options.embeddedInLabelledBy?.hidden ||
!!options.embeddedInDescribedBy?.hidden ||
!!options.embeddedInNativeTextAlternative?.hidden ||
!!options.embeddedInLabel?.hidden;
if (isElementIgnoredForAria(element) ||
(!isEmbeddedInHiddenReferenceTraversal && isElementHiddenForAria(element))) {
options.visitedElements.add(element);
return '';
}
}
const labelledBy = getAriaLabelledByElements(element);

View file

@ -15,14 +15,12 @@
*/
import net from 'net';
import path from 'path';
import http2 from 'http2';
import type https from 'https';
import fs from 'fs';
import tls from 'tls';
import stream from 'stream';
import { createSocket, createTLSSocket } from '../utils/happy-eyeballs';
import { escapeHTML, ManualPromise, rewriteErrorMessage } from '../utils';
import { escapeHTML, generateSelfSignedCertificate, ManualPromise, rewriteErrorMessage } from '../utils';
import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../common/socksProxy';
import { SocksProxy } from '../common/socksProxy';
import type * as channels from '@protocol/channels';
@ -32,10 +30,8 @@ let dummyServerTlsOptions: tls.TlsOptions | undefined = undefined;
function loadDummyServerCertsIfNeeded() {
if (dummyServerTlsOptions)
return;
dummyServerTlsOptions = {
key: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/key.pem')),
cert: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/cert.pem')),
};
const { cert, key } = generateSelfSignedCertificate();
dummyServerTlsOptions = { key, cert };
}
class ALPNCache {
@ -161,7 +157,6 @@ class SocksProxyConnection {
let targetTLS: tls.TLSSocket | undefined = undefined;
const handleError = (error: Error) => {
error = rewriteOpenSSLErrorIfNeeded(error);
debugLogger.log('client-certificates', `error when connecting to target: ${error.message.replaceAll('\n', ' ')}`);
const responseBody = escapeHTML('Playwright client-certificate error: ' + error.message)
.replaceAll('\n', ' <br>');
@ -202,14 +197,6 @@ class SocksProxyConnection {
}
};
let secureContext: tls.SecureContext;
try {
secureContext = tls.createSecureContext(clientCertificatesToTLSOptions(this.socksProxy.clientCertificates, new URL(`https://${this.host}:${this.port}`).origin));
} catch (error) {
handleError(error);
return;
}
if (this._closed) {
internalTLS.destroy();
return;
@ -221,7 +208,7 @@ class SocksProxyConnection {
rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors,
ALPNProtocols: [internalTLS.alpnProtocol || 'http/1.1'],
servername: !net.isIP(this.host) ? this.host : undefined,
secureContext,
secureContext: this.socksProxy.secureContextMap.get(new URL(`https://${this.host}:${this.port}`).origin),
});
targetTLS.once('secureConnect', () => {
@ -240,7 +227,7 @@ export class ClientCertificatesProxy {
_socksProxy: SocksProxy;
private _connections: Map<string, SocksProxyConnection> = new Map();
ignoreHTTPSErrors: boolean | undefined;
clientCertificates: channels.BrowserNewContextOptions['clientCertificates'];
secureContextMap: Map<string, tls.SecureContext> = new Map();
alpnCache: ALPNCache;
constructor(
@ -248,7 +235,7 @@ export class ClientCertificatesProxy {
) {
this.alpnCache = new ALPNCache();
this.ignoreHTTPSErrors = contextOptions.ignoreHTTPSErrors;
this.clientCertificates = contextOptions.clientCertificates;
this._initSecureContexts(contextOptions.clientCertificates);
this._socksProxy = new SocksProxy();
this._socksProxy.setPattern('*');
this._socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => {
@ -270,6 +257,27 @@ export class ClientCertificatesProxy {
loadDummyServerCertsIfNeeded();
}
_initSecureContexts(clientCertificates: channels.BrowserNewContextOptions['clientCertificates']) {
// Step 1. Group certificates by origin.
const origin2certs = new Map<string, channels.BrowserNewContextOptions['clientCertificates']>();
for (const cert of clientCertificates || []) {
const origin = normalizeOrigin(cert.origin);
const certs = origin2certs.get(origin) || [];
certs.push(cert);
origin2certs.set(origin, certs);
}
// Step 2. Create secure contexts for each origin.
for (const [origin, certs] of origin2certs) {
try {
this.secureContextMap.set(origin, tls.createSecureContext(convertClientCertificatesToTLSOptions(certs)));
} catch (error) {
error = rewriteOpenSSLErrorIfNeeded(error);
throw rewriteErrorMessage(error, `Failed to load client certificate: ${error.message}`);
}
}
}
public async listen(): Promise<string> {
const port = await this._socksProxy.listen(0, '127.0.0.1');
return `socks5://127.0.0.1:${port}`;
@ -280,25 +288,25 @@ export class ClientCertificatesProxy {
}
}
export function clientCertificatesToTLSOptions(
clientCertificates: channels.BrowserNewContextOptions['clientCertificates'],
origin: string
function normalizeOrigin(origin: string): string {
try {
return new URL(origin).origin;
} catch (error) {
return origin;
}
}
function convertClientCertificatesToTLSOptions(
clientCertificates: channels.BrowserNewContextOptions['clientCertificates']
): Pick<https.RequestOptions, 'pfx' | 'key' | 'cert'> | undefined {
const matchingCerts = clientCertificates?.filter(c => {
try {
return new URL(c.origin).origin === origin;
} catch (error) {
return c.origin === origin;
}
});
if (!matchingCerts || !matchingCerts.length)
if (!clientCertificates || !clientCertificates.length)
return;
const tlsOptions = {
pfx: [] as { buf: Buffer, passphrase?: string }[],
key: [] as { pem: Buffer, passphrase?: string }[],
cert: [] as Buffer[],
};
for (const cert of matchingCerts) {
for (const cert of clientCertificates) {
if (cert.cert)
tlsOptions.cert.push(cert.cert);
if (cert.key)
@ -309,6 +317,16 @@ export function clientCertificatesToTLSOptions(
return tlsOptions;
}
export function getMatchingTLSOptionsForOrigin(
clientCertificates: channels.BrowserNewContextOptions['clientCertificates'],
origin: string
): Pick<https.RequestOptions, 'pfx' | 'key' | 'cert'> | undefined {
const matchingCerts = clientCertificates?.filter(c =>
normalizeOrigin(c.origin) === origin
);
return convertClientCertificatesToTLSOptions(matchingCerts);
}
function rewriteToLocalhostIfNeeded(host: string): string {
return host === 'local.playwright' ? 'localhost' : host;
}

View file

@ -15,6 +15,7 @@
*/
import crypto from 'crypto';
import { assert } from './debug';
export function createGuid(): string {
return crypto.randomBytes(16).toString('hex');
@ -25,3 +26,170 @@ export function calculateSha1(buffer: Buffer | string): string {
hash.update(buffer);
return hash.digest('hex');
}
// Variable-length quantity encoding aka. base-128 encoding
function encodeBase128(value: number): Buffer {
const bytes = [];
do {
let byte = value & 0x7f;
value >>>= 7;
if (bytes.length > 0) byte |= 0x80;
bytes.push(byte);
} while (value > 0);
return Buffer.from(bytes.reverse());
};
// ASN1/DER Speficiation: https://www.itu.int/rec/T-REC-X.680-X.693-202102-I/en
class DER {
static encodeSequence(data: Buffer[]): Buffer {
return this._encode(0x30, Buffer.concat(data));
}
static encodeInteger(data: number): Buffer {
assert(data >= -128 && data <= 127);
return this._encode(0x02, Buffer.from([data]));
}
static encodeObjectIdentifier(oid: string): Buffer {
const parts = oid.split('.').map((v) => Number(v));
// Encode the second part, which could be large, using base-128 encoding if necessary
const output = [encodeBase128(40 * parts[0] + parts[1])];
for (let i = 2; i < parts.length; i++) {
output.push(encodeBase128(parts[i]));
}
return this._encode(0x06, Buffer.concat(output));
}
static encodeNull(): Buffer {
return Buffer.from([0x05, 0x00]);
}
static encodeSet(data: Buffer[]): Buffer {
assert(data.length === 1, 'Only one item in the set is supported. We\'d need to sort the data to support more.');
// We expect the data to be already sorted.
return this._encode(0x31, Buffer.concat(data));
}
static encodeExplicitContextDependent(tag: number, data: Buffer): Buffer {
return this._encode(0xa0 + tag, data);
}
static encodePrintableString(data: string): Buffer {
return this._encode(0x13, Buffer.from(data));
}
static encodeBitString(data: Buffer): Buffer {
// The first byte of the content is the number of unused bits at the end
const unusedBits = 0; // Assuming all bits are used
const content = Buffer.concat([Buffer.from([unusedBits]), data]);
return this._encode(0x03, content);
}
static encodeDate(date: Date): Buffer {
const year = date.getUTCFullYear();
const isGeneralizedTime = year >= 2050;
const parts = [
isGeneralizedTime ? year.toString() : year.toString().slice(-2),
(date.getUTCMonth() + 1).toString().padStart(2, '0'),
date.getUTCDate().toString().padStart(2, '0'),
date.getUTCHours().toString().padStart(2, '0'),
date.getUTCMinutes().toString().padStart(2, '0'),
date.getUTCSeconds().toString().padStart(2, '0')
];
const encodedDate = parts.join('') + 'Z';
const tag = isGeneralizedTime ? 0x18 : 0x17; // 0x18 for GeneralizedTime, 0x17 for UTCTime
return this._encode(tag, Buffer.from(encodedDate));
}
private static _encode(tag: number, data: Buffer): Buffer {
const lengthBytes = this._encodeLength(data.length);
return Buffer.concat([Buffer.from([tag]), lengthBytes, data]);
}
private static _encodeLength(length: number): Buffer {
if (length < 128) {
return Buffer.from([length]);
} else {
const lengthBytes = [];
while (length > 0) {
lengthBytes.unshift(length & 0xFF);
length >>= 8;
}
return Buffer.from([0x80 | lengthBytes.length, ...lengthBytes]);
}
}
}
// X.509 Specification: https://datatracker.ietf.org/doc/html/rfc2459#section-4.1
export function generateSelfSignedCertificate() {
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
const publicKeyDer = publicKey.export({ type: 'pkcs1', format: 'der' });
const oneYearInMilliseconds = 365 * 24 * 60 * 60 * 1_000;
const notBefore = new Date(new Date().getTime() - oneYearInMilliseconds);
const notAfter = new Date(new Date().getTime() + oneYearInMilliseconds);
// List of fields / structure: https://datatracker.ietf.org/doc/html/rfc2459#section-4.1
const tbsCertificate = DER.encodeSequence([
DER.encodeExplicitContextDependent(0, DER.encodeInteger(1)), // version
DER.encodeInteger(1), // serialNumber
DER.encodeSequence([
DER.encodeObjectIdentifier('1.2.840.113549.1.1.11'), // sha256WithRSAEncryption PKCS #1
DER.encodeNull()
]), // signature
DER.encodeSequence([
DER.encodeSet([
DER.encodeSequence([
DER.encodeObjectIdentifier('2.5.4.3'), // commonName X.520 DN component
DER.encodePrintableString('localhost')
]),
]),
DER.encodeSet([
DER.encodeSequence([
DER.encodeObjectIdentifier('2.5.4.10'), // organizationName X.520 DN component
DER.encodePrintableString('Playwright Client Certificate Support')
])
])
]), // issuer
DER.encodeSequence([
DER.encodeDate(notBefore), // notBefore
DER.encodeDate(notAfter), // notAfter
]), // validity
DER.encodeSequence([
DER.encodeSet([
DER.encodeSequence([
DER.encodeObjectIdentifier('2.5.4.3'), // commonName X.520 DN component
DER.encodePrintableString('localhost')
]),
]),
DER.encodeSet([
DER.encodeSequence([
DER.encodeObjectIdentifier('2.5.4.10'), // organizationName X.520 DN component
DER.encodePrintableString('Playwright Client Certificate Support')
])
])
]), // subject
DER.encodeSequence([
DER.encodeSequence([
DER.encodeObjectIdentifier('1.2.840.113549.1.1.1'), // rsaEncryption PKCS #1
DER.encodeNull()
]),
DER.encodeBitString(publicKeyDer)
]), // SubjectPublicKeyInfo
]);
const signature = crypto.sign('sha256', tbsCertificate, privateKey);
const certificate = DER.encodeSequence([
tbsCertificate,
DER.encodeSequence([
DER.encodeObjectIdentifier('1.2.840.113549.1.1.11'), // sha256WithRSAEncryption PKCS #1
DER.encodeNull()
]),
DER.encodeBitString(signature)
]);
const certPem = [
'-----BEGIN CERTIFICATE-----',
// Split the base64 string into lines of 64 characters
certificate.toString('base64').match(/.{1,64}/g)!.join('\n'),
'-----END CERTIFICATE-----'
].join('\n');
return {
cert: certPem,
key: privateKey.export({ type: 'pkcs1', format: 'pem' }),
};
}

View file

@ -288,8 +288,41 @@ export interface Page {
* [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not
* defined.
*
* **Bundling**
*
* If you have a complex script split into several files, it needs to be bundled into a single file first. We
* recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a
* commonjs module and pass `path` and `arg`.
*
* ```js
* // mocks/mockRandom.ts
* // This script can import other files.
* import { defaultValue } from './defaultValue';
*
* export default function(value?: number) {
* window.Math.random = () => value ?? defaultValue;
* }
* ```
*
* ```js
* // tests/example.spec.ts
* const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') };
*
* // Passing 42 as an argument to the default export function.
* await page.addInitScript({ path: mockPath }, 42);
*
* // Make sure to pass undefined even if you do not need to pass an argument.
* // This instructs Playwright to treat the file as a commonjs module.
* await page.addInitScript({ path: mockPath }, undefined);
* ```
*
* @param script Script to be evaluated in the page.
* @param arg Optional argument to pass to `script` (only supported when passing a function).
* @param arg Optional JSON-serializable argument to pass to `script`.
* - When `script` is a function, the argument is passed to it directly.
* - When `script` is a file path, the file is assumed to be a commonjs module. The default export, either
* `module.exports` or `module.exports.default`, should be a function that's going to be executed with this
* argument.
*/
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<void>;
@ -7666,8 +7699,41 @@ export interface BrowserContext {
* [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not
* defined.
*
* **Bundling**
*
* If you have a complex script split into several files, it needs to be bundled into a single file first. We
* recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a
* commonjs module and pass `path` and `arg`.
*
* ```js
* // mocks/mockRandom.ts
* // This script can import other files.
* import { defaultValue } from './defaultValue';
*
* export default function(value?: number) {
* window.Math.random = () => value ?? defaultValue;
* }
* ```
*
* ```js
* // tests/example.spec.ts
* const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') };
*
* // Passing 42 as an argument to the default export function.
* await context.addInitScript({ path: mockPath }, 42);
*
* // Make sure to pass undefined even if you do not need to pass an argument.
* // This instructs Playwright to treat the file as a commonjs module.
* await context.addInitScript({ path: mockPath }, undefined);
* ```
*
* @param script Script to be evaluated in all pages in the browser context.
* @param arg Optional argument to pass to `script` (only supported when passing a function).
* @param arg Optional JSON-serializable argument to pass to `script`.
* - When `script` is a function, the argument is passed to it directly.
* - When `script` is a file path, the file is assumed to be a commonjs module. The default export, either
* `module.exports` or `module.exports.default`, should be a function that's going to be executed with this
* argument.
*/
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<void>;
@ -9138,10 +9204,10 @@ export interface Browser {
*
* **Details**
*
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
* certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that
* the certificate is valid for.
* An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`,
* a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally,
* `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided
* with an exact match to the request origin that the certificate is valid for.
*
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
*
@ -9159,16 +9225,31 @@ export interface Browser {
*/
certPath?: string;
/**
* Direct value of the certificate in PEM format.
*/
cert?: Buffer;
/**
* Path to the file with the private key in PEM format.
*/
keyPath?: string;
/**
* Direct value of the private key in PEM format.
*/
key?: Buffer;
/**
* Path to the PFX or PKCS12 encoded private key and certificate chain.
*/
pfxPath?: string;
/**
* Direct value of the PFX or PKCS12 encoded private key and certificate chain.
*/
pfx?: Buffer;
/**
* Passphrase for the private key (PEM or PFX).
*/
@ -13850,10 +13931,10 @@ export interface BrowserType<Unused = {}> {
*
* **Details**
*
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
* certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that
* the certificate is valid for.
* An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`,
* a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally,
* `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided
* with an exact match to the request origin that the certificate is valid for.
*
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
*
@ -13871,16 +13952,31 @@ export interface BrowserType<Unused = {}> {
*/
certPath?: string;
/**
* Direct value of the certificate in PEM format.
*/
cert?: Buffer;
/**
* Path to the file with the private key in PEM format.
*/
keyPath?: string;
/**
* Direct value of the private key in PEM format.
*/
key?: Buffer;
/**
* Path to the PFX or PKCS12 encoded private key and certificate chain.
*/
pfxPath?: string;
/**
* Direct value of the PFX or PKCS12 encoded private key and certificate chain.
*/
pfx?: Buffer;
/**
* Passphrase for the private key (PEM or PFX).
*/
@ -16259,10 +16355,10 @@ export interface APIRequest {
*
* **Details**
*
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
* certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that
* the certificate is valid for.
* An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`,
* a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally,
* `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided
* with an exact match to the request origin that the certificate is valid for.
*
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
*
@ -16280,16 +16376,31 @@ export interface APIRequest {
*/
certPath?: string;
/**
* Direct value of the certificate in PEM format.
*/
cert?: Buffer;
/**
* Path to the file with the private key in PEM format.
*/
keyPath?: string;
/**
* Direct value of the private key in PEM format.
*/
key?: Buffer;
/**
* Path to the PFX or PKCS12 encoded private key and certificate chain.
*/
pfxPath?: string;
/**
* Direct value of the PFX or PKCS12 encoded private key and certificate chain.
*/
pfx?: Buffer;
/**
* Passphrase for the private key (PEM or PFX).
*/
@ -20600,10 +20711,10 @@ export interface BrowserContextOptions {
*
* **Details**
*
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
* certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that
* the certificate is valid for.
* An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`,
* a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally,
* `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided
* with an exact match to the request origin that the certificate is valid for.
*
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
*
@ -20621,16 +20732,31 @@ export interface BrowserContextOptions {
*/
certPath?: string;
/**
* Direct value of the certificate in PEM format.
*/
cert?: Buffer;
/**
* Path to the file with the private key in PEM format.
*/
keyPath?: string;
/**
* Direct value of the private key in PEM format.
*/
key?: Buffer;
/**
* Path to the PFX or PKCS12 encoded private key and certificate chain.
*/
pfxPath?: string;
/**
* Direct value of the PFX or PKCS12 encoded private key and certificate chain.
*/
pfx?: Buffer;
/**
* Passphrase for the private key (PEM or PFX).
*/

View file

@ -55,7 +55,7 @@ export interface MountResultJsx extends Locator {
export const test: TestType<{
mount<HooksConfig>(
component: JSX.Element,
options: MountOptionsJsx<HooksConfig>
options?: MountOptionsJsx<HooksConfig>
): Promise<MountResultJsx>;
mount<HooksConfig, Component = unknown>(
component: Component,

View file

@ -248,6 +248,11 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
}, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any],
_setupArtifacts: [async ({ playwright, screenshot }, use, testInfo) => {
// This fixture has a separate zero-timeout slot to ensure that artifact collection
// happens even after some fixtures or hooks time out.
// Now that default test timeout is known, we can replace zero with an actual value.
testInfo.setTimeout(testInfo.project.timeout);
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);
const csiListener: ClientInstrumentationListener = {
@ -297,7 +302,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
clientInstrumentation.removeListener(csiListener);
await artifactsRecorder.didFinishTest();
}, { auto: 'all-hooks-included', title: 'trace recording', box: true } as any],
}, { auto: 'all-hooks-included', title: 'trace recording', box: true, timeout: 0 } as any],
_contextFactory: [async ({ browser, video, _reuseContext, _combinedContextOptions /** mitigate dep-via-auto lack of traceability */ }, use, testInfo) => {
const testInfoImpl = testInfo as TestInfoImpl;

View file

@ -63,7 +63,7 @@ export class TestRun {
export function createTaskRunner(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner<TestRun> {
const taskRunner = TaskRunner.create<TestRun>(reporters, config.config.globalTimeout);
addGlobalSetupTasks(taskRunner, config);
taskRunner.addTask('load tests', createLoadTask('in-process', { filterOnly: true, filterOnlyChanged: true, failOnLoadErrors: true }));
taskRunner.addTask('load tests', createLoadTask('in-process', { filterOnly: true, failOnLoadErrors: true }));
addRunTasks(taskRunner, config);
return taskRunner;
}
@ -76,14 +76,14 @@ export function createTaskRunnerForWatchSetup(config: FullConfigInternal, report
export function createTaskRunnerForWatch(config: FullConfigInternal, reporters: ReporterV2[], additionalFileMatcher?: Matcher): TaskRunner<TestRun> {
const taskRunner = TaskRunner.create<TestRun>(reporters);
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, filterOnlyChanged: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true, additionalFileMatcher }));
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true, additionalFileMatcher }));
addRunTasks(taskRunner, config);
return taskRunner;
}
export function createTaskRunnerForTestServer(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner<TestRun> {
const taskRunner = TaskRunner.create<TestRun>(reporters);
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, filterOnlyChanged: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true }));
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true }));
addRunTasks(taskRunner, config);
return taskRunner;
}
@ -108,7 +108,7 @@ function addRunTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal
export function createTaskRunnerForList(config: FullConfigInternal, reporters: ReporterV2[], mode: 'in-process' | 'out-of-process', options: { failOnLoadErrors: boolean }): TaskRunner<TestRun> {
const taskRunner = TaskRunner.create<TestRun>(reporters, config.config.globalTimeout);
taskRunner.addTask('load tests', createLoadTask(mode, { ...options, filterOnly: false, filterOnlyChanged: false }));
taskRunner.addTask('load tests', createLoadTask(mode, { ...options, filterOnly: false }));
taskRunner.addTask('report begin', createReportBeginTask());
return taskRunner;
}
@ -222,14 +222,14 @@ function createListFilesTask(): Task<TestRun> {
};
}
function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, filterOnlyChanged: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task<TestRun> {
function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task<TestRun> {
return {
setup: async (reporter, testRun, errors, softErrors) => {
await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter, options.additionalFileMatcher);
await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors);
let cliOnlyChangedMatcher: Matcher | undefined = undefined;
if (testRun.config.cliOnlyChanged && options.filterOnlyChanged) {
if (testRun.config.cliOnlyChanged) {
for (const plugin of testRun.config.plugins)
await plugin.instance?.populateDependencies?.();
const changedFiles = await detectChangedTestFiles(testRun.config.cliOnlyChanged, testRun.config.configDir);

View file

@ -5206,10 +5206,10 @@ export interface PlaywrightTestOptions {
*
* **Details**
*
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
* certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that
* the certificate is valid for.
* An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`,
* a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally,
* `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided
* with an exact match to the request origin that the certificate is valid for.
*
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
*

View file

@ -171,7 +171,7 @@ export const Recorder: React.FC<RecorderProps> = ({
sidebarSize={200}
main={<CodeMirrorWrapper text={source.text} language={source.language} highlight={source.highlight} revealLine={source.revealLine} readOnly={true} lineNumbers={true} />}
sidebar={<TabbedPane
rightToolbar={selectedTab === 'locator' ? [<ToolbarButton icon='files' title='Copy' onClick={() => copy(locator)} />] : []}
rightToolbar={selectedTab === 'locator' ? [<ToolbarButton key={1} icon='files' title='Copy' onClick={() => copy(locator)} />] : []}
tabs={[
{
id: 'locator',

View file

@ -85,7 +85,9 @@ export class SnapshotServer {
contentType = `${contentType}; charset=utf-8`;
const headers = new Headers();
headers.set('Content-Type', contentType);
// "x-unknown" in the har means "no content type".
if (contentType !== 'x-unknown')
headers.set('Content-Type', contentType);
for (const { name, value } of resource.response.headers)
headers.set(name, value);
headers.delete('Content-Encoding');

View file

@ -114,9 +114,11 @@ export class TraceModel {
async resourceForSha1(sha1: string): Promise<Blob | undefined> {
const blob = await this._backend.readBlob('resources/' + sha1);
if (!blob)
return;
return new Blob([blob], { type: this._resourceToContentType.get(sha1) || 'application/octet-stream' });
const contentType = this._resourceToContentType.get(sha1);
// "x-unknown" in the har means "no content type".
if (!blob || contentType === undefined || contentType === 'x-unknown')
return blob;
return new Blob([blob], { type: contentType });
}
storage(): SnapshotStorage {

View file

@ -126,7 +126,7 @@ export const AttachmentsTab: React.FunctionComponent<{
const url = attachmentURL(a);
return <div className='attachment-item' key={`screenshot-${i}`}>
<div><img draggable='false' src={url} /></div>
<div><a target='_blank' href={url}>{a.name}</a></div>
<div><a target='_blank' href={url} rel='noreferrer'>{a.name}</a></div>
</div>;
})}
{attachments.size ? <div className='attachments-section'>Attachments</div> : undefined}

View file

@ -213,6 +213,7 @@ function format(args: { preview: string, value: any }[]): JSX.Element[] {
}
function formatAnsi(text: string): JSX.Element[] {
// eslint-disable-next-line react/jsx-key
return [<span dangerouslySetInnerHTML={{ __html: ansi2html(text.trim()) }}></span>];
}

View file

@ -26,10 +26,10 @@ export type FilterState = {
export const defaultFilterState: FilterState = { searchValue: '', resourceType: 'All' };
export const NetworkFilters: React.FunctionComponent<{
export const NetworkFilters = ({ filterState, onFilterStateChange }: {
filterState: FilterState,
onFilterStateChange: (filterState: FilterState) => void,
}> = ({ filterState, onFilterStateChange }) => {
}) => {
return (
<div className='network-filters'>
<input

View file

@ -48,6 +48,22 @@
overflow: hidden;
}
.network-font-preview {
font-family: font-preview;
font-size: 30px;
line-height: 40px;
padding: 16px;
padding-left: 6px;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
.network-font-preview-error {
margin-top: 8px;
text-align: center;
}
.tab-network .toolbar {
min-height: 30px !important;
background-color: initial !important;

View file

@ -29,7 +29,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{
return <TabbedPane
dataTestId='network-request-details'
leftToolbar={[<ToolbarButton icon='close' title='Close' onClick={onClose}></ToolbarButton>]}
leftToolbar={[<ToolbarButton key='close' icon='close' title='Close' onClick={onClose}></ToolbarButton>]}
tabs={[
{
id: 'request',
@ -101,12 +101,13 @@ const ResponseTab: React.FunctionComponent<{
const BodyTab: React.FunctionComponent<{
resource: ResourceSnapshot;
}> = ({ resource }) => {
const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, mimeType?: string } | null>(null);
const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, mimeType?: string, font?: BinaryData } | null>(null);
React.useEffect(() => {
const readResources = async () => {
if (resource.response.content._sha1) {
const useBase64 = resource.response.content.mimeType.includes('image');
const isFont = resource.response.content.mimeType.includes('font');
const response = await fetch(`sha1/${resource.response.content._sha1}`);
if (useBase64) {
const blob = await response.blob();
@ -114,6 +115,9 @@ const BodyTab: React.FunctionComponent<{
const eventPromise = new Promise<any>(f => reader.onload = f);
reader.readAsDataURL(blob);
setResponseBody({ dataUrl: (await eventPromise).target.result });
} else if (isFont) {
const font = await response.arrayBuffer();
setResponseBody({ font });
} else {
const formattedBody = formatBody(await response.text(), resource.response.content.mimeType);
setResponseBody({ text: formattedBody, mimeType: resource.response.content.mimeType });
@ -128,11 +132,48 @@ const BodyTab: React.FunctionComponent<{
return <div className='network-request-details-tab'>
{!resource.response.content._sha1 && <div>Response body is not available for this request.</div>}
{responseBody && responseBody.font && <FontPreview font={responseBody.font} />}
{responseBody && responseBody.dataUrl && <img draggable='false' src={responseBody.dataUrl} />}
{responseBody && responseBody.text && <CodeMirrorWrapper text={responseBody.text} mimeType={responseBody.mimeType} readOnly lineNumbers={true}/>}
</div>;
};
const FontPreview: React.FunctionComponent<{
font: BinaryData;
}> = ({ font }) => {
const [isError, setIsError] = React.useState(false);
React.useEffect(() => {
let fontFace: FontFace;
try {
// note: constant font family name will lead to bugs
// when displaying two font previews.
fontFace = new FontFace('font-preview', font);
if (fontFace.status === 'loaded')
document.fonts.add(fontFace);
if (fontFace.status === 'error')
setIsError(true);
} catch {
setIsError(true);
}
return () => {
document.fonts.delete(fontFace);
};
}, [font]);
if (isError)
return <div className='network-font-preview-error'>Could not load font preview</div>;
return <div className='network-font-preview'>
ABCDEFGHIJKLM<br />
NOPQRSTUVWXYZ<br />
abcdefghijklm<br />
nopqrstuvwxyz<br />
1234567890
</div>;
};
function statusClass(statusCode: number): string {
if (statusCode < 300 || statusCode === 304)
return 'green-circle';

View file

@ -184,6 +184,7 @@ export const SnapshotTab: React.FunctionComponent<{
<ToolbarButton className='pick-locator' title='Pick locator' icon='target' toggled={isInspecting} onClick={() => setIsInspecting(!isInspecting)} />
{['action', 'before', 'after'].map(tab => {
return <TabbedPaneTab
key={tab}
id={tab}
title={renderTitle(tab)}
selected={snapshotTab === tab}

View file

@ -17,7 +17,7 @@
import { clsx } from '@web/uiUtils';
import './tag.css';
export const TagView: React.FC<{ tag: string, style?: React.CSSProperties, onClick?: (e: React.MouseEvent) => void }> = ({ tag, style, onClick }) => {
export const TagView = ({ tag, style, onClick }: { tag: string, style?: React.CSSProperties, onClick?: (e: React.MouseEvent) => void }) => {
return <span
className={clsx('tag', `tag-color-${tagNameToColor(tag)}`)}
onClick={onClick}

View file

@ -60,7 +60,7 @@ export const FiltersView: React.FC<{
{expanded && <div className='hbox' style={{ marginLeft: 14, maxHeight: 200, overflowY: 'auto' }}>
<div className='filter-list'>
{[...statusFilters.entries()].map(([status, value]) => {
return <div className='filter-entry'>
return <div className='filter-entry' key={status}>
<label>
<input type='checkbox' checked={value} onClick={() => {
const copy = new Map(statusFilters);
@ -74,7 +74,7 @@ export const FiltersView: React.FC<{
</div>
<div className='filter-list'>
{[...projectFilters.entries()].map(([projectName, value]) => {
return <div className='filter-entry'>
return <div className='filter-entry' key={projectName}>
<label>
<input type='checkbox' checked={value} onClick={() => {
const copy = new Map(projectFilters);

View file

@ -76,6 +76,7 @@ export function GridView<T>(model: GridViewProps<T>) {
<div className='grid-view-header'>
{model.columns.map((column, i) => {
return <div
key={model.columnTitle(column)}
className={'grid-view-header-cell ' + sortingHeader(column, model.sorting)}
style={{
width: i < model.columns.length - 1 ? model.columnWidths.get(column) : undefined,
@ -97,6 +98,7 @@ export function GridView<T>(model: GridViewProps<T>) {
{model.columns.map((column, i) => {
const { body, title } = model.render(item, column, index);
return <div
key={model.columnTitle(column)}
className={`grid-view-cell grid-view-column-${String(column)}`}
title={title}
style={{

View file

@ -152,6 +152,7 @@ export function ListView<T>({
onMouseEnter={() => setHighlightedItem(item)}
onMouseLeave={() => setHighlightedItem(undefined)}
>
{/* eslint-disable-next-line react/jsx-key */}
{indentation ? new Array(indentation).fill(0).map(() => <div className='list-view-indent'></div>) : undefined}
{icon && <div
className={'codicon ' + (icon(item, index) || 'codicon-blank')}

View file

@ -48,6 +48,7 @@ export const TabbedPane: React.FunctionComponent<{
{mode === 'default' && <div style={{ flex: 'auto', display: 'flex', height: '100%', overflow: 'hidden' }}>
{[...tabs.map(tab => (
<TabbedPaneTab
key={tab.id}
id={tab.id}
title={tab.title}
count={tab.count}
@ -67,7 +68,7 @@ export const TabbedPane: React.FunctionComponent<{
suffix = ` (${tab.count})`;
if (tab.errorCount)
suffix = ` (${tab.errorCount})`;
return <option value={tab.id} selected={tab.id === selectedTab}>{tab.title}{suffix}</option>;
return <option key={tab.id} value={tab.id} selected={tab.id === selectedTab}>{tab.title}{suffix}</option>;
})}
</select>
</div>}

View file

@ -51,7 +51,7 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
title={title}
disabled={!!disabled}
style={style}
data-testId={testId}
data-testid={testId}
>
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
{children}

View file

@ -119,9 +119,9 @@ export const ImageDiffView: React.FC<{
</div>}
</div>
<div style={{ alignSelf: 'start', lineHeight: '18px', marginLeft: '15px' }}>
<div>{diff.diff && <a target='_blank' href={diff.diff.attachment.path}>{diff.diff.attachment.name}</a>}</div>
<div><a target={noTargetBlank ? '' : '_blank'} href={diff.actual!.attachment.path}>{diff.actual!.attachment.name}</a></div>
<div><a target={noTargetBlank ? '' : '_blank'} href={diff.expected!.attachment.path}>{diff.expected!.attachment.name}</a></div>
<div>{diff.diff && <a target='_blank' href={diff.diff.attachment.path} rel='noreferrer'>{diff.diff.attachment.name}</a>}</div>
<div><a target={noTargetBlank ? '' : '_blank'} href={diff.actual!.attachment.path} rel='noreferrer'>{diff.actual!.attachment.name}</a></div>
<div><a target={noTargetBlank ? '' : '_blank'} href={diff.expected!.attachment.path} rel='noreferrer'>{diff.expected!.attachment.name}</a></div>
</div>
</>}
</div>;

View file

@ -82,6 +82,7 @@ export const ResizeView: React.FC<{
/>}
{offsets.map((offset, index) => {
return <div
key={index}
style={{
...dividerStyle,
top: orientation === 'horizontal' ? 0 : offset,

View file

@ -9,21 +9,20 @@
}
.codicon {
font: normal normal normal 16px/1 codicon;
flex: none;
display: inline-block;
text-decoration: none;
text-rendering: auto;
text-align: center;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
font: normal normal normal 16px/1 codicon;
flex: none;
display: inline-block;
text-decoration: none;
text-rendering: auto;
text-align: center;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.codicon-blank:before { content: '\2003'; }
.codicon-add:before { content: '\ea60'; }
.codicon-plus:before { content: '\ea60'; }
.codicon-gist-new:before { content: '\ea60'; }
@ -39,6 +38,7 @@
.codicon-record-keys:before { content: '\ea65'; }
.codicon-keyboard:before { content: '\ea65'; }
.codicon-tag:before { content: '\ea66'; }
.codicon-git-pull-request-label:before { content: '\ea66'; }
.codicon-tag-add:before { content: '\ea66'; }
.codicon-tag-remove:before { content: '\ea66'; }
.codicon-person:before { content: '\ea67'; }
@ -74,6 +74,7 @@
.codicon-debug-breakpoint:before { content: '\ea71'; }
.codicon-debug-breakpoint-disabled:before { content: '\ea71'; }
.codicon-debug-hint:before { content: '\ea71'; }
.codicon-terminal-decoration-success:before { content: '\ea71'; }
.codicon-primitive-square:before { content: '\ea72'; }
.codicon-edit:before { content: '\ea73'; }
.codicon-pencil:before { content: '\ea73'; }
@ -185,7 +186,6 @@
.codicon-check:before { content: '\eab2'; }
.codicon-checklist:before { content: '\eab3'; }
.codicon-chevron-down:before { content: '\eab4'; }
.codicon-drop-down-button:before { content: '\eab4'; }
.codicon-chevron-left:before { content: '\eab5'; }
.codicon-chevron-right:before { content: '\eab6'; }
.codicon-chevron-up:before { content: '\eab7'; }
@ -193,9 +193,10 @@
.codicon-chrome-maximize:before { content: '\eab9'; }
.codicon-chrome-minimize:before { content: '\eaba'; }
.codicon-chrome-restore:before { content: '\eabb'; }
.codicon-circle:before { content: '\eabc'; }
.codicon-circle-outline:before { content: '\eabc'; }
.codicon-circle:before { content: '\eabc'; }
.codicon-debug-breakpoint-unverified:before { content: '\eabc'; }
.codicon-terminal-decoration-incomplete:before { content: '\eabc'; }
.codicon-circle-slash:before { content: '\eabd'; }
.codicon-circuit-board:before { content: '\eabe'; }
.codicon-clear-all:before { content: '\eabf'; }
@ -207,7 +208,6 @@
.codicon-collapse-all:before { content: '\eac5'; }
.codicon-color-mode:before { content: '\eac6'; }
.codicon-comment-discussion:before { content: '\eac7'; }
.codicon-compare-changes:before { content: '\eafd'; }
.codicon-credit-card:before { content: '\eac9'; }
.codicon-dash:before { content: '\eacc'; }
.codicon-dashboard:before { content: '\eacd'; }
@ -231,6 +231,7 @@
.codicon-diff-removed:before { content: '\eadf'; }
.codicon-diff-renamed:before { content: '\eae0'; }
.codicon-diff:before { content: '\eae1'; }
.codicon-diff-sidebyside:before { content: '\eae1'; }
.codicon-discard:before { content: '\eae2'; }
.codicon-editor-layout:before { content: '\eae3'; }
.codicon-empty-window:before { content: '\eae4'; }
@ -259,6 +260,7 @@
.codicon-gist:before { content: '\eafb'; }
.codicon-git-commit:before { content: '\eafc'; }
.codicon-git-compare:before { content: '\eafd'; }
.codicon-compare-changes:before { content: '\eafd'; }
.codicon-git-merge:before { content: '\eafe'; }
.codicon-github-action:before { content: '\eaff'; }
.codicon-github-alt:before { content: '\eb00'; }
@ -271,13 +273,11 @@
.codicon-horizontal-rule:before { content: '\eb07'; }
.codicon-hubot:before { content: '\eb08'; }
.codicon-inbox:before { content: '\eb09'; }
.codicon-issue-closed:before { content: '\eba4'; }
.codicon-issue-reopened:before { content: '\eb0b'; }
.codicon-issues:before { content: '\eb0c'; }
.codicon-italic:before { content: '\eb0d'; }
.codicon-jersey:before { content: '\eb0e'; }
.codicon-json:before { content: '\eb0f'; }
.codicon-bracket:before { content: '\eb0f'; }
.codicon-kebab-vertical:before { content: '\eb10'; }
.codicon-key:before { content: '\eb11'; }
.codicon-law:before { content: '\eb12'; }
@ -295,6 +295,7 @@
.codicon-megaphone:before { content: '\eb1e'; }
.codicon-mention:before { content: '\eb1f'; }
.codicon-milestone:before { content: '\eb20'; }
.codicon-git-pull-request-milestone:before { content: '\eb20'; }
.codicon-mortar-board:before { content: '\eb21'; }
.codicon-move:before { content: '\eb22'; }
.codicon-multiple-windows:before { content: '\eb23'; }
@ -355,7 +356,6 @@
.codicon-star-half:before { content: '\eb5a'; }
.codicon-symbol-class:before { content: '\eb5b'; }
.codicon-symbol-color:before { content: '\eb5c'; }
.codicon-symbol-customcolor:before { content: '\eb5c'; }
.codicon-symbol-constant:before { content: '\eb5d'; }
.codicon-symbol-enum-member:before { content: '\eb5e'; }
.codicon-symbol-field:before { content: '\eb5f'; }
@ -407,6 +407,7 @@
.codicon-debug-stackframe-active:before { content: '\eb89'; }
.codicon-circle-small-filled:before { content: '\eb8a'; }
.codicon-debug-stackframe-dot:before { content: '\eb8a'; }
.codicon-terminal-decoration-mark:before { content: '\eb8a'; }
.codicon-debug-stackframe:before { content: '\eb8b'; }
.codicon-debug-stackframe-focused:before { content: '\eb8b'; }
.codicon-debug-breakpoint-unsupported:before { content: '\eb8c'; }
@ -414,14 +415,17 @@
.codicon-debug-reverse-continue:before { content: '\eb8e'; }
.codicon-debug-step-back:before { content: '\eb8f'; }
.codicon-debug-restart-frame:before { content: '\eb90'; }
.codicon-debug-alt:before { content: '\eb91'; }
.codicon-call-incoming:before { content: '\eb92'; }
.codicon-call-outgoing:before { content: '\eb93'; }
.codicon-menu:before { content: '\eb94'; }
.codicon-expand-all:before { content: '\eb95'; }
.codicon-feedback:before { content: '\eb96'; }
.codicon-git-pull-request-reviewer:before { content: '\eb96'; }
.codicon-group-by-ref-type:before { content: '\eb97'; }
.codicon-ungroup-by-ref-type:before { content: '\eb98'; }
.codicon-account:before { content: '\eb99'; }
.codicon-git-pull-request-assignee:before { content: '\eb99'; }
.codicon-bell-dot:before { content: '\eb9a'; }
.codicon-debug-console:before { content: '\eb9b'; }
.codicon-library:before { content: '\eb9c'; }
@ -430,10 +434,10 @@
.codicon-sync-ignored:before { content: '\eb9f'; }
.codicon-pinned:before { content: '\eba0'; }
.codicon-github-inverted:before { content: '\eba1'; }
.codicon-debug-alt:before { content: '\eb91'; }
.codicon-server-process:before { content: '\eba2'; }
.codicon-server-environment:before { content: '\eba3'; }
.codicon-pass:before { content: '\eba4'; }
.codicon-issue-closed:before { content: '\eba4'; }
.codicon-stop-circle:before { content: '\eba5'; }
.codicon-play-circle:before { content: '\eba6'; }
.codicon-record:before { content: '\eba7'; }
@ -466,7 +470,7 @@
.codicon-debug-rerun:before { content: '\ebc0'; }
.codicon-workspace-trusted:before { content: '\ebc1'; }
.codicon-workspace-untrusted:before { content: '\ebc2'; }
.codicon-workspace-unspecified:before { content: '\ebc3'; }
.codicon-workspace-unknown:before { content: '\ebc3'; }
.codicon-terminal-cmd:before { content: '\ebc4'; }
.codicon-terminal-debian:before { content: '\ebc5'; }
.codicon-terminal-linux:before { content: '\ebc6'; }
@ -500,6 +504,7 @@
.codicon-graph-line:before { content: '\ebe2'; }
.codicon-graph-scatter:before { content: '\ebe3'; }
.codicon-pie-chart:before { content: '\ebe4'; }
.codicon-bracket:before { content: '\eb0f'; }
.codicon-bracket-dot:before { content: '\ebe5'; }
.codicon-bracket-error:before { content: '\ebe6'; }
.codicon-lock-small:before { content: '\ebe7'; }
@ -519,20 +524,26 @@
.codicon-layout-statusbar:before { content: '\ebf5'; }
.codicon-layout-menubar:before { content: '\ebf6'; }
.codicon-layout-centered:before { content: '\ebf7'; }
.codicon-layout-sidebar-right-off:before { content: '\ec00'; }
.codicon-layout-panel-off:before { content: '\ec01'; }
.codicon-layout-sidebar-left-off:before { content: '\ec02'; }
.codicon-target:before { content: '\ebf8'; }
.codicon-indent:before { content: '\ebf9'; }
.codicon-record-small:before { content: '\ebfa'; }
.codicon-error-small:before { content: '\ebfb'; }
.codicon-terminal-decoration-error:before { content: '\ebfb'; }
.codicon-arrow-circle-down:before { content: '\ebfc'; }
.codicon-arrow-circle-left:before { content: '\ebfd'; }
.codicon-arrow-circle-right:before { content: '\ebfe'; }
.codicon-arrow-circle-up:before { content: '\ebff'; }
.codicon-layout-sidebar-right-off:before { content: '\ec00'; }
.codicon-layout-panel-off:before { content: '\ec01'; }
.codicon-layout-sidebar-left-off:before { content: '\ec02'; }
.codicon-blank:before { content: '\ec03'; }
.codicon-heart-filled:before { content: '\ec04'; }
.codicon-map:before { content: '\ec05'; }
.codicon-map-horizontal:before { content: '\ec05'; }
.codicon-fold-horizontal:before { content: '\ec05'; }
.codicon-map-filled:before { content: '\ec06'; }
.codicon-map-horizontal-filled:before { content: '\ec06'; }
.codicon-fold-horizontal-filled:before { content: '\ec06'; }
.codicon-circle-small:before { content: '\ec07'; }
.codicon-bell-slash:before { content: '\ec08'; }
.codicon-bell-slash-dot:before { content: '\ec09'; }
@ -544,3 +555,42 @@
.codicon-send:before { content: '\ec0f'; }
.codicon-sparkle:before { content: '\ec10'; }
.codicon-insert:before { content: '\ec11'; }
.codicon-mic:before { content: '\ec12'; }
.codicon-thumbsdown-filled:before { content: '\ec13'; }
.codicon-thumbsup-filled:before { content: '\ec14'; }
.codicon-coffee:before { content: '\ec15'; }
.codicon-snake:before { content: '\ec16'; }
.codicon-game:before { content: '\ec17'; }
.codicon-vr:before { content: '\ec18'; }
.codicon-chip:before { content: '\ec19'; }
.codicon-piano:before { content: '\ec1a'; }
.codicon-music:before { content: '\ec1b'; }
.codicon-mic-filled:before { content: '\ec1c'; }
.codicon-repo-fetch:before { content: '\ec1d'; }
.codicon-copilot:before { content: '\ec1e'; }
.codicon-lightbulb-sparkle:before { content: '\ec1f'; }
.codicon-robot:before { content: '\ec20'; }
.codicon-sparkle-filled:before { content: '\ec21'; }
.codicon-diff-single:before { content: '\ec22'; }
.codicon-diff-multiple:before { content: '\ec23'; }
.codicon-surround-with:before { content: '\ec24'; }
.codicon-share:before { content: '\ec25'; }
.codicon-git-stash:before { content: '\ec26'; }
.codicon-git-stash-apply:before { content: '\ec27'; }
.codicon-git-stash-pop:before { content: '\ec28'; }
.codicon-vscode:before { content: '\ec29'; }
.codicon-vscode-insiders:before { content: '\ec2a'; }
.codicon-code-oss:before { content: '\ec2b'; }
.codicon-run-coverage:before { content: '\ec2c'; }
.codicon-run-all-coverage:before { content: '\ec2d'; }
.codicon-coverage:before { content: '\ec2e'; }
.codicon-github-project:before { content: '\ec2f'; }
.codicon-map-vertical:before { content: '\ec30'; }
.codicon-fold-vertical:before { content: '\ec30'; }
.codicon-map-vertical-filled:before { content: '\ec31'; }
.codicon-fold-vertical-filled:before { content: '\ec31'; }
.codicon-go-to-search:before { content: '\ec32'; }
.codicon-percentage:before { content: '\ec33'; }
.codicon-sort-percentage:before { content: '\ec33'; }
.codicon-attach:before { content: '\ec34'; }
.codicon-git-fetch:before { content: '\f101'; }

Binary file not shown.

View file

@ -0,0 +1,33 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// one.ts
var one_exports = {};
__export(one_exports, {
default: () => one_default
});
module.exports = __toCommonJS(one_exports);
// two.ts
var value = 42;
// one.ts
function one_default(arg) {
window.injected = arg ?? value;
}

View file

@ -20,7 +20,7 @@ export default function Fetcher() {
}, [fetched, setFetched, setData]);
return <div>
<div data-testId='name'>{data.name}</div>
<div data-testid='name'>{data.name}</div>
<button onClick={() => {
setFetched(false);
setData({ name: '<none>' });

View file

@ -15,6 +15,7 @@
* limitations under the License.
*/
import os from 'os';
import { browserTest as it, expect } from '../config/browserTest';
it.describe('mobile viewport', () => {
@ -54,7 +55,8 @@ it.describe('mobile viewport', () => {
}
});
it('should be detectable by Modernizr', async ({ playwright, browser, server }) => {
it('should be detectable by Modernizr', async ({ playwright, browser, server, browserName, platform }) => {
it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'detect-touch.html uses Modernizr which uses WebGL. WebGL is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277');
const iPhone = playwright.devices['iPhone 6'];
const context = await browser.newContext({ ...iPhone });
const page = await context.newPage();
@ -63,7 +65,8 @@ it.describe('mobile viewport', () => {
await context.close();
});
it('should detect touch when applying viewport with touches', async ({ browser, server }) => {
it('should detect touch when applying viewport with touches', async ({ browser, server, browserName, platform }) => {
it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'Modernizr uses WebGL. WebGL is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277');
const context = await browser.newContext({ viewport: { width: 800, height: 600 }, hasTouch: true });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);

View file

@ -93,7 +93,8 @@ it('should emulate availWidth and availHeight', async ({ page }) => {
expect(await page.evaluate(() => window.screen.availHeight)).toBe(600);
});
it('should not have touch by default', async ({ page, server }) => {
it('should not have touch by default', async ({ page, server, browserName, platform }) => {
it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'detect-touch.html uses Modernizr which uses WebGL. WebGL is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277');
await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false);
await page.goto(server.PREFIX + '/detect-touch.html');

View file

@ -16,6 +16,8 @@
import fs from 'fs';
import tls from 'tls';
import type https from 'https';
import zlib from 'zlib';
import type http2 from 'http2';
import type http from 'http';
import { expect, playwrightTest as base } from '../config/browserTest';
@ -303,6 +305,21 @@ test.describe('browser', () => {
await page.close();
});
test('should pass with matching certificates when passing as content', async ({ browser, startCCServer, asset, browserName }) => {
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
const page = await browser.newPage({
ignoreHTTPSErrors: true,
clientCertificates: [{
origin: new URL(serverURL).origin,
cert: await fs.promises.readFile(asset('client-certificates/client/trusted/cert.pem')),
key: await fs.promises.readFile(asset('client-certificates/client/trusted/key.pem')),
}],
});
await page.goto(serverURL);
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
await page.close();
});
test('should not hang on tls errors during TLS 1.2 handshake', async ({ browser, asset, platform, browserName }) => {
for (const tlsVersion of ['TLSv1.3', 'TLSv1.2'] as const) {
await test.step(`TLS version: ${tlsVersion}`, async () => {
@ -360,34 +377,175 @@ test.describe('browser', () => {
await page.close();
});
test('should fail with matching certificates in legacy pfx format', async ({ browser, startCCServer, asset, browserName }) => {
test('should handle TLS renegotiation with client certificates', async ({ browser, asset, browserName, platform }) => {
const server: https.Server = createHttpsServer({
key: fs.readFileSync(asset('client-certificates/server/server_key.pem')),
cert: fs.readFileSync(asset('client-certificates/server/server_cert.pem')),
ca: [fs.readFileSync(asset('client-certificates/server/server_cert.pem'))],
requestCert: false, // Initially don't request client cert
rejectUnauthorized: false,
// TLSv1.3 does not support renegotiation
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.2',
});
server.on('request', async (req, res) => {
if (!req.socket)
return;
const renegotiate = () => new Promise<void>((resolve, reject) => {
(req.socket as tls.TLSSocket).renegotiate({
requestCert: true,
rejectUnauthorized: false
}, err => {
if (err)
reject(err);
else
resolve();
});
});
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html', 'connection': 'close' });
res.end();
} else if (req.url === '/from-fetch-api') {
res.writeHead(200, {
'Content-Type': 'text/plain',
'Transfer-Encoding': 'chunked'
});
res.flushHeaders();
await new Promise<void>(resolve => req.once('data', data => {
res.write(`server received: ${data.toString()}\n`);
resolve();
}));
await renegotiate();
for (let i = 0; i < 4; i++) {
res.write(`${i}-from-server\n`);
// Best-effort to trigger a new chunk
await new Promise<void>(resolve => setTimeout(resolve, 100));
}
res.end('server closed the connection');
} else if (req.url === '/style.css') {
res.writeHead(200, {
'Content-Type': 'text/css',
'Content-Encoding': 'gzip',
'Transfer-Encoding': 'chunked'
});
await renegotiate();
const stylesheet = `
button {
background-color: red;
}
`;
const stylesheetBuffer = await new Promise<Buffer>((resolve, reject) => {
zlib.gzip(stylesheet, (err, buffer) => {
if (err)
reject(err);
else
resolve(buffer);
});
});
for (let i = 0; i < stylesheetBuffer.length; i += 100) {
res.write(stylesheetBuffer.slice(i, i + 100));
// Best-effort to trigger a new chunk
await new Promise<void>(resolve => setTimeout(resolve, 20));
}
res.end();
} else {
res.writeHead(404);
res.end();
}
});
await new Promise<void>(resolve => server.listen(0, 'localhost', resolve));
const port = (server.address() as import('net').AddressInfo).port;
const origin = 'https://' + (browserName === 'webkit' && platform === 'darwin' ? 'local.playwright' : 'localhost');
const serverUrl = `${origin}:${port}`;
const context = await browser.newContext({
ignoreHTTPSErrors: true,
clientCertificates: [{
origin,
certPath: asset('client-certificates/client/trusted/cert.pem'),
keyPath: asset('client-certificates/client/trusted/key.pem'),
}],
});
const page = await context.newPage();
await test.step('fetch API', async () => {
await page.goto(serverUrl);
const response = await page.evaluate(async () => {
const response = await fetch('/from-fetch-api', {
method: 'POST',
body: 'client-request-payload'
});
return await response.text();
});
expect(response).toBe([
'server received: client-request-payload',
'0-from-server',
'1-from-server',
'2-from-server',
'3-from-server',
'server closed the connection'
].join('\n'));
});
await test.step('Gzip encoded CSS Stylesheet', async () => {
await page.goto(serverUrl);
// The <link> would throw with net::ERR_INVALID_CHUNKED_ENCODING
await page.setContent(`
<button>Click me</button>
<link rel="stylesheet" href="/style.css">
`);
await expect(page.locator('button')).toHaveCSS('background-color', /* red */'rgb(255, 0, 0)');
});
await context.close();
await new Promise<void>(resolve => server.close(() => resolve()));
});
test('should pass with matching certificates in pfx format when passing as content', async ({ browser, startCCServer, asset, browserName }) => {
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
const page = await browser.newPage({
ignoreHTTPSErrors: true,
clientCertificates: [{
origin: new URL(serverURL).origin,
pfx: await fs.promises.readFile(asset('client-certificates/client/trusted/cert.pfx')),
passphrase: 'secure'
}],
});
await page.goto(serverURL);
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
await page.close();
});
test('should fail with matching certificates in legacy pfx format', async ({ browser, startCCServer, asset, browserName }) => {
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
await expect(browser.newPage({
ignoreHTTPSErrors: true,
clientCertificates: [{
origin: new URL(serverURL).origin,
pfxPath: asset('client-certificates/client/trusted/cert-legacy.pfx'),
passphrase: 'secure'
}],
});
await page.goto(serverURL);
await expect(page.getByText('Unsupported TLS certificate.')).toBeVisible();
await page.close();
})).rejects.toThrow('Unsupported TLS certificate');
});
test('should throw a http error if the pfx passphrase is incorect', async ({ browser, startCCServer, asset, browserName }) => {
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
const page = await browser.newPage({
await expect(browser.newPage({
ignoreHTTPSErrors: true,
clientCertificates: [{
origin: new URL(serverURL).origin,
pfxPath: asset('client-certificates/client/trusted/cert.pfx'),
passphrase: 'this-password-is-incorrect'
}],
});
await page.goto(serverURL);
await expect(page.getByText('Playwright client-certificate error: mac verify failure')).toBeVisible();
await page.close();
})).rejects.toThrow('Failed to load client certificate: mac verify failure');
});
test('should pass with matching certificates on context APIRequestContext instance', async ({ browser, startCCServer, asset, browserName }) => {

View file

@ -32,7 +32,8 @@ async function checkFeatures(name: string, context: any, server: any) {
it('safari-14-1', async ({ browser, browserName, platform, server, headless, isMac }) => {
it.skip(browserName !== 'webkit');
it.skip(browserName === 'webkit' && parseInt(os.release(), 10) < 20, 'WebKit for macOS 10.15 is frozen.');
it.skip(browserName === 'webkit' && platform === 'darwin', 'WebKit for macOS 10.15 is frozen.');
it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'Modernizr uses WebGL which is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277');
const context = await browser.newContext({
deviceScaleFactor: 2
});
@ -81,7 +82,8 @@ it('safari-14-1', async ({ browser, browserName, platform, server, headless, isM
it('mobile-safari-14-1', async ({ playwright, browser, browserName, platform, isMac, server, headless }) => {
it.skip(browserName !== 'webkit');
it.skip(browserName === 'webkit' && parseInt(os.release(), 10) < 20, 'WebKit for macOS 10.15 is frozen.');
it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) < 20, 'WebKit for macOS 10.15 is frozen.');
it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'Modernizr uses WebGL which is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277');
const iPhone = playwright.devices['iPhone 12'];
const context = await browser.newContext(iPhone);
const { actual, expected } = await checkFeatures('mobile-safari-14-1', context, server);

View file

@ -462,6 +462,39 @@ test('should work with form and tricky input names', async ({ page }) => {
expect.soft(await getNameAndRole(page, 'form')).toEqual({ role: 'form', name: 'my form' });
});
test('should ignore stylesheet from hidden aria-labelledby subtree', async ({ page }) => {
await page.setContent(`
<div id=mylabel style="display:none">
<template shadowrootmode=open>
<style>span { color: red; }</style>
<span>hello</span>
</template>
</div>
<input aria-labelledby=mylabel type=text>
`);
expect.soft(await getNameAndRole(page, 'input')).toEqual({ role: 'textbox', name: 'hello' });
});
test('should not include hidden pseudo into accessible name', async ({ page }) => {
await page.setContent(`
<style>
span:before {
content: 'world';
display: none;
}
div:after {
content: 'bye';
visibility: hidden;
}
</style>
<a href="http://example.com">
<span>hello</span>
<div>hello</div>
</a>
`);
expect.soft(await getNameAndRole(page, 'a')).toEqual({ role: 'link', name: 'hello hello' });
});
function toArray(x: any): any[] {
return Array.isArray(x) ? x : [x];
}

View file

@ -1439,3 +1439,15 @@ test('should show baseURL in metadata pane', {
await traceViewer.showMetadataTab();
await expect(traceViewer.metadataTab).toContainText('baseURL:https://example.com');
});
test('should serve css without content-type', async ({ page, runAndTrace, server }) => {
server.setRoute('/one-style.css', (req, res) => {
res.writeHead(200);
res.end(`body { background: red }`);
});
const traceViewer = await runAndTrace(async () => {
await page.goto(server.PREFIX + '/one-style.html');
});
const snapshotFrame = await traceViewer.snapshotFrame('page.goto');
await expect(snapshotFrame.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)', { timeout: 0 });
});

View file

@ -31,6 +31,18 @@ it('should work with a path', async ({ page, server, asset }) => {
expect(await page.evaluate(() => window['result'])).toBe(123);
});
it('should assume CJS module with a path and arg', async ({ page, server, asset }) => {
await page.addInitScript({ path: asset('injectedmodule.js') }, 17);
await page.goto(server.EMPTY_PAGE);
expect(await page.evaluate(() => window['injected'])).toBe(17);
});
it('should assume CJS module with a path and undefined arg', async ({ page, server, asset }) => {
await page.addInitScript({ path: asset('injectedmodule.js') }, undefined);
await page.goto(server.EMPTY_PAGE);
expect(await page.evaluate(() => window['injected'])).toBe(42);
});
it('should work with content @smoke', async ({ page, server }) => {
await page.addInitScript({ content: 'window["injected"] = 123' });
await page.goto(server.PREFIX + '/tamperable.html');

View file

@ -181,7 +181,9 @@ it('should work with Cross-Origin-Opener-Policy after redirect', async ({ page,
it('should properly cancel Cross-Origin-Opener-Policy navigation', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32107' },
}, async ({ page, server }) => {
}, async ({ page, server, browserName, isLinux, headless }) => {
it.fixme(browserName === 'webkit' && isLinux, 'Started failing after https://commits.webkit.org/281488@main');
it.fixme(browserName === 'chromium' && headless, 'COOP navigation cancels the one that starts later');
server.setRoute('/empty.html', (req, res) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.end();

View file

@ -323,6 +323,7 @@ it.describe('page screenshot', () => {
it('should work for webgl', async ({ page, server, browserName, platform }) => {
it.fixme(browserName === 'firefox');
it.fixme(browserName === 'chromium' && platform === 'darwin' && os.arch() === 'arm64', 'SwiftShader is not available on macOS-arm64 - https://github.com/microsoft/playwright/issues/28216');
it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'WebGL is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277');
await page.setViewportSize({ width: 640, height: 480 });
await page.goto(server.PREFIX + '/screenshots/webgl.html');

View file

@ -414,6 +414,29 @@ test('should run project dependencies of changed tests', {
expect(result.output).toContain('setup test is executed');
});
test('should work with list mode', async ({ runInlineTest, git, writeFiles }) => {
await writeFiles({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('fails', () => { expect(1).toBe(2); });
`,
});
git(`add .`);
git(`commit -m init`);
const result = await runInlineTest({
'b.spec.ts': `
import { test, expect } from '@playwright/test';
test('fails', () => { expect(1).toBe(2); });
`
}, { 'only-changed': true, 'list': true });
expect(result.exitCode).toBe(0);
expect(result.output).toContain('b.spec.ts');
expect(result.output).not.toContain('a.spec.ts');
});
test('exits successfully if there are no changes', async ({ runInlineTest, git, writeFiles }) => {
await writeFiles({
'a.spec.ts': `
@ -428,4 +451,5 @@ test('exits successfully if there are no changes', async ({ runInlineTest, git,
const result = await runInlineTest({}, { 'only-changed': true });
expect(result.exitCode).toBe(0);
});
});

View file

@ -1225,4 +1225,44 @@ test('should not nest top level expect into unfinished api calls ', {
]);
});
test('should record trace after fixture teardown timeout', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30718' },
}, async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
fixture: async ({}, use) => {
await use('foo');
await new Promise(() => {});
},
});
// Note: it is important that "fixture" is last, so that it runs the teardown first.
test('fails', async ({ page, fixture }) => {
await page.evaluate(() => console.log('from the page'));
});
`,
}, { trace: 'on', timeout: '3000' }, { DEBUG: 'pw:test' });
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
const tracePath = test.info().outputPath('test-results', 'a-fails', 'trace.zip');
const trace = await parseTrace(tracePath);
expect(trace.actionTree).toEqual([
'Before Hooks',
' fixture: browser',
' browserType.launch',
' fixture: context',
' browser.newContext',
' fixture: page',
' browserContext.newPage',
' fixture: fixture',
'page.evaluate',
'After Hooks',
' fixture: fixture',
'Worker Cleanup',
' fixture: browser',
]);
// Check console events to make sure that library trace is recorded.
expect(trace.events).toContainEqual(expect.objectContaining({ type: 'console', text: 'from the page' }));
});

View file

@ -4,7 +4,7 @@ set -x
trap "cd $(pwd -P)" EXIT
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
NODE_VERSION="20.16.0" # autogenerated via ./update-playwright-driver-version.mjs
NODE_VERSION="20.17.0" # autogenerated via ./update-playwright-driver-version.mjs
cd "$(dirname "$0")"
PACKAGE_VERSION=$(node -p "require('../../package.json').version")

View file

@ -353,6 +353,8 @@ class Member {
this.clazz = null;
/** @type {Member=} */
this.enclosingMethod = undefined;
/** @type {Member=} */
this.parent = undefined;
this.async = false;
this.alias = name;
this.overloadIndex = 0;
@ -372,10 +374,11 @@ class Member {
this.args = new Map();
if (this.kind === 'method')
this.enclosingMethod = this;
const indexType = type => {
type.deepProperties().forEach(p => {
const indexArg = (/** @type {Member} */ arg) => {
arg.type?.deepProperties().forEach(p => {
p.enclosingMethod = this;
indexType(p.type);
p.parent = arg;
indexArg(p);
});
}
for (const arg of this.argsArray) {
@ -385,7 +388,7 @@ class Member {
// @ts-ignore
arg.type.properties.sort((p1, p2) => p1.name.localeCompare(p2.name));
}
indexType(arg.type);
indexArg(arg);
}
}

View file

@ -77,7 +77,7 @@ function serializeMember(member) {
}
function serializeProperty(arg) {
const result = { ...arg };
const result = { ...arg, parent: undefined };
sanitize(result);
if (arg.type)
result.type = serializeType(arg.type, arg.name === 'options');