Merge branch 'main' into only-changed-no-tests-exit-1
This commit is contained in:
commit
94b0181b22
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
390
docs/src/ci.md
390
docs/src/ci.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
861
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
```
|
||||
|
|
@ -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-----
|
||||
|
|
@ -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-----
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' }),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
162
packages/playwright-core/types/types.d.ts
vendored
162
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -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).
|
||||
*/
|
||||
|
|
|
|||
2
packages/playwright-ct-vue/index.d.ts
vendored
2
packages/playwright-ct-vue/index.d.ts
vendored
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
8
packages/playwright/types/test.d.ts
vendored
8
packages/playwright/types/test.d.ts
vendored
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ export const ResizeView: React.FC<{
|
|||
/>}
|
||||
{offsets.map((offset, index) => {
|
||||
return <div
|
||||
key={index}
|
||||
style={{
|
||||
...dividerStyle,
|
||||
top: orientation === 'horizontal' ? 0 : offset,
|
||||
|
|
|
|||
98
packages/web/src/third_party/vscode/codicon.css
vendored
98
packages/web/src/third_party/vscode/codicon.css
vendored
|
|
@ -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'; }
|
||||
|
|
|
|||
BIN
packages/web/src/third_party/vscode/codicon.ttf
vendored
BIN
packages/web/src/third_party/vscode/codicon.ttf
vendored
Binary file not shown.
33
tests/assets/injectedmodule.js
Normal file
33
tests/assets/injectedmodule.js
Normal 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;
|
||||
}
|
||||
|
|
@ -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>' });
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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': `
|
||||
|
|
@ -429,3 +452,4 @@ test('exits successfully if there are no changes', async ({ runInlineTest, git,
|
|||
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in a new issue