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",
|
sourceType: "module",
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
|
"plugin:react/recommended",
|
||||||
"plugin:react-hooks/recommended"
|
"plugin:react-hooks/recommended"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
react: { version: "18" }
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ESLint rules
|
* ESLint rules
|
||||||
*
|
*
|
||||||
|
|
@ -124,5 +129,8 @@ module.exports = {
|
||||||
"mustMatch": "Copyright",
|
"mustMatch": "Copyright",
|
||||||
"templateFile": require("path").join(__dirname, "utils", "copyright.js"),
|
"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.
|
[`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
|
### param: BrowserContext.addInitScript.script
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
* langs: js
|
* langs: js
|
||||||
- `script` <[function]|[string]|[Object]>
|
- `script` <[function]|[string]|[Object]>
|
||||||
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
|
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
|
||||||
current working directory. Optional.
|
current working directory.
|
||||||
- `content` ?<[string]> Raw script content. Optional.
|
- `content` ?<[string]> Raw script content.
|
||||||
|
|
||||||
Script to be evaluated in all pages in the browser context.
|
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
|
* langs: js
|
||||||
- `arg` ?<[Serializable]>
|
- `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
|
### param: BrowserContext.addInitScript.path
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
|
|
|
||||||
|
|
@ -619,13 +619,42 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
|
||||||
[`method: Page.addInitScript`] is not defined.
|
[`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
|
### param: Page.addInitScript.script
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
* langs: js
|
* langs: js
|
||||||
- `script` <[function]|[string]|[Object]>
|
- `script` <[function]|[string]|[Object]>
|
||||||
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
|
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
|
||||||
current working directory. Optional.
|
current working directory.
|
||||||
- `content` ?<[string]> Raw script content. Optional.
|
- `content` ?<[string]> Raw script content.
|
||||||
|
|
||||||
Script to be evaluated in the page.
|
Script to be evaluated in the page.
|
||||||
|
|
||||||
|
|
@ -641,7 +670,9 @@ Script to be evaluated in all pages in the browser context.
|
||||||
* langs: js
|
* langs: js
|
||||||
- `arg` ?<[Serializable]>
|
- `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
|
### param: Page.addInitScript.path
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
|
|
|
||||||
|
|
@ -531,15 +531,18 @@ Does not enforce fixed viewport, allows resizing window in the headed mode.
|
||||||
- `clientCertificates` <[Array]<[Object]>>
|
- `clientCertificates` <[Array]<[Object]>>
|
||||||
- `origin` <[string]> Exact origin that the certificate is valid for. Origin includes `https` protocol, a hostname and optionally a port.
|
- `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.
|
- `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.
|
- `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.
|
- `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).
|
- `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX).
|
||||||
|
|
||||||
TLS Client Authentication allows the server to request a client certificate and verify it.
|
TLS Client Authentication allows the server to request a client certificate and verify it.
|
||||||
|
|
||||||
**Details**
|
**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
|
:::note
|
||||||
Using Client Certificates in combination with Proxy Servers is not supported.
|
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.
|
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
|
### 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
|
### Use parallelism and sharding
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -461,7 +461,7 @@ Playwright's Firefox version matches the recent [Firefox Stable](https://www.moz
|
||||||
|
|
||||||
### WebKit
|
### 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
|
## Install behind a firewall or a proxy
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
---
|
---
|
||||||
id: ci-intro
|
id: ci-intro
|
||||||
title: "CI GitHub Actions"
|
title: "Setting up CI"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
* langs: js
|
* 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).
|
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).
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
#### You will learn
|
#### You will learn
|
||||||
* langs: js
|
* 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 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 HTML report](/ci-intro.md#viewing-the-html-report)
|
||||||
- [How to view the trace](/ci-intro.md#viewing-the-trace)
|
- [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.
|
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
|
#### You will learn
|
||||||
* langs: python, java, csharp
|
* 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 test logs](/ci-intro.md#viewing-test-logs)
|
||||||
- [How to view the trace](/ci-intro.md#viewing-the-trace)
|
- [How to view the trace](/ci-intro.md#viewing-the-trace)
|
||||||
|
|
||||||
|
|
||||||
## Setting up GitHub Actions
|
## Setting up GitHub Actions
|
||||||
|
|
||||||
### On push/pull_request
|
|
||||||
* langs: js
|
* 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"
|
```yml js title=".github/workflows/playwright.yml"
|
||||||
name: Playwright Tests
|
name: Playwright Tests
|
||||||
|
|
@ -57,7 +51,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: lts/*
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
|
|
@ -72,10 +66,21 @@ jobs:
|
||||||
retention-days: 30
|
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
|
* 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"
|
```yml python title=".github/workflows/playwright.yml"
|
||||||
name: Playwright Tests
|
name: Playwright Tests
|
||||||
|
|
@ -128,7 +133,7 @@ jobs:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Build & Install
|
- name: Build & Install
|
||||||
run: mvn -B install -D skipTests --no-transfer-progress
|
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"
|
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps"
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: mvn test
|
run: mvn test
|
||||||
|
|
@ -151,275 +156,23 @@ jobs:
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 8.0.x
|
dotnet-version: 8.0.x
|
||||||
- run: dotnet build
|
- name: Build & Install
|
||||||
|
run: dotnet build
|
||||||
- name: Ensure browsers are installed
|
- name: Ensure browsers are installed
|
||||||
run: pwsh bin/Debug/net8.0/playwright.ps1 install --with-deps
|
run: pwsh bin/Debug/net8.0/playwright.ps1 install --with-deps
|
||||||
- name: Run your tests
|
- name: Run your tests
|
||||||
run: dotnet test
|
run: dotnet test
|
||||||
```
|
```
|
||||||
|
|
||||||
### On push/pull_request (sharded)
|
To learn more about this, see ["Understanding GitHub Actions"](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions).
|
||||||
* 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.
|
Looking at the list of steps in `jobs.test.steps`, you can see that the workflow performs these steps:
|
||||||
|
|
||||||
### Via Containers
|
1. Clone your repository
|
||||||
|
2. Install language dependencies
|
||||||
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.
|
3. Install project dependencies and build
|
||||||
|
4. Install Playwright Browsers
|
||||||
```yml js title=".github/workflows/playwright.yml"
|
5. Run tests
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Create a Repo and Push to GitHub
|
## 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 perform Actions](./input.md)
|
||||||
- [Learn how to write Assertions](./test-assertions.md)
|
- [Learn how to write Assertions](./test-assertions.md)
|
||||||
- [Learn more about the Trace Viewer](/trace-viewer.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
|
## 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
|
### 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
|
### 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
|
## 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
|
```js
|
||||||
// A primitive value.
|
// A primitive value.
|
||||||
|
|
@ -86,7 +172,7 @@ await page.evaluate(object => object.foo, { foo: 'bar' });
|
||||||
const button = await page.evaluateHandle('window.button');
|
const button = await page.evaluateHandle('window.button');
|
||||||
await page.evaluate(button => button.textContent, 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);
|
await button.evaluate((button, from) => button.textContent.substring(from), 5);
|
||||||
|
|
||||||
// Object with multiple handles.
|
// Object with multiple handles.
|
||||||
|
|
@ -109,7 +195,7 @@ await page.evaluate(
|
||||||
([b1, b2]) => b1.textContent + b2.textContent,
|
([b1, b2]) => b1.textContent + b2.textContent,
|
||||||
[button1, button2]);
|
[button1, button2]);
|
||||||
|
|
||||||
// Any non-cyclic mix of serializables and handles works.
|
// Any mix of serializables and handles works.
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
x => x.button1.textContent + x.list[0].textContent + String(x.foo),
|
x => x.button1.textContent + x.list[0].textContent + String(x.foo),
|
||||||
{ button1, list: [button2], foo: null });
|
{ button1, list: [button2], foo: null });
|
||||||
|
|
@ -131,7 +217,7 @@ page.evaluate("object => object.foo", obj);
|
||||||
ElementHandle button = page.evaluateHandle("window.button");
|
ElementHandle button = page.evaluateHandle("window.button");
|
||||||
page.evaluate("button => button.textContent", 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);
|
button.evaluate("(button, from) => button.textContent.substring(from)", 5);
|
||||||
|
|
||||||
// Object with multiple handles.
|
// Object with multiple handles.
|
||||||
|
|
@ -156,7 +242,7 @@ page.evaluate(
|
||||||
"([b1, b2]) => b1.textContent + b2.textContent",
|
"([b1, b2]) => b1.textContent + b2.textContent",
|
||||||
Arrays.asList(button1, button2));
|
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<>();
|
Map<String, Object> arg = new HashMap<>();
|
||||||
arg.put("button1", button1);
|
arg.put("button1", button1);
|
||||||
arg.put("list", Arrays.asList(button2));
|
arg.put("list", Arrays.asList(button2));
|
||||||
|
|
@ -180,7 +266,7 @@ await page.evaluate('object => object.foo', { 'foo': 'bar' })
|
||||||
button = await page.evaluate_handle('button')
|
button = await page.evaluate_handle('button')
|
||||||
await page.evaluate('button => button.textContent', 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)
|
await button.evaluate('(button, from) => button.textContent.substring(from)', 5)
|
||||||
|
|
||||||
# Object with multiple handles.
|
# Object with multiple handles.
|
||||||
|
|
@ -203,7 +289,7 @@ await page.evaluate("""
|
||||||
([b1, b2]) => b1.textContent + b2.textContent""",
|
([b1, b2]) => b1.textContent + b2.textContent""",
|
||||||
[button1, button2])
|
[button1, button2])
|
||||||
|
|
||||||
# Any non-cyclic mix of serializables and handles works.
|
# Any mix of serializables and handles works.
|
||||||
await page.evaluate("""
|
await page.evaluate("""
|
||||||
x => x.button1.textContent + x.list[0].textContent + String(x.foo)""",
|
x => x.button1.textContent + x.list[0].textContent + String(x.foo)""",
|
||||||
{ 'button1': button1, 'list': [button2], 'foo': None })
|
{ 'button1': button1, 'list': [button2], 'foo': None })
|
||||||
|
|
@ -223,7 +309,7 @@ page.evaluate('object => object.foo', { 'foo': 'bar' })
|
||||||
button = page.evaluate_handle('window.button')
|
button = page.evaluate_handle('window.button')
|
||||||
page.evaluate('button => button.textContent', 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)
|
button.evaluate('(button, from) => button.textContent.substring(from)', 5)
|
||||||
|
|
||||||
# Object with multiple handles.
|
# Object with multiple handles.
|
||||||
|
|
@ -245,7 +331,7 @@ page.evaluate("""
|
||||||
([b1, b2]) => b1.textContent + b2.textContent""",
|
([b1, b2]) => b1.textContent + b2.textContent""",
|
||||||
[button1, button2])
|
[button1, button2])
|
||||||
|
|
||||||
# Any non-cyclic mix of serializables and handles works.
|
# Any mix of serializables and handles works.
|
||||||
page.evaluate("""
|
page.evaluate("""
|
||||||
x => x.button1.textContent + x.list[0].textContent + String(x.foo)""",
|
x => x.button1.textContent + x.list[0].textContent + String(x.foo)""",
|
||||||
{ 'button1': button1, 'list': [button2], 'foo': None })
|
{ '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");
|
var button = await page.EvaluateHandleAsync("window.button");
|
||||||
await page.EvaluateAsync<IJSHandle>("button => button.textContent", 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);
|
await button.EvaluateAsync<string>("(button, from) => button.textContent.substring(from)", 5);
|
||||||
|
|
||||||
// Object with multiple handles.
|
// Object with multiple handles.
|
||||||
|
|
@ -282,93 +368,69 @@ await page.EvaluateAsync("({ button1, button2 }) => button1.textContent + button
|
||||||
// Note the required parenthesis.
|
// Note the required parenthesis.
|
||||||
await page.EvaluateAsync("([b1, b2]) => b1.textContent + b2.textContent", new[] { button1, button2 });
|
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 });
|
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
|
```js
|
||||||
const data = { text: 'some data', value: 1 };
|
import { test, expect } from '@playwright/test';
|
||||||
// Pass |data| as a parameter.
|
import path from 'path';
|
||||||
const result = await page.evaluate(data => {
|
|
||||||
window.myApp.use(data);
|
|
||||||
}, data);
|
|
||||||
```
|
|
||||||
|
|
||||||
```java
|
test.beforeEach(async ({ page }) => {
|
||||||
Map<String, Object> data = new HashMap<>();
|
// Add script for every test in the beforeEach hook.
|
||||||
data.put("text", "some data");
|
// Make sure to correctly resolve the script path.
|
||||||
data.put("value", 1);
|
await page.addInitScript({ path: path.resolve(__dirname, '../mocks/preload.js') });
|
||||||
// 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);
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
```java
|
```java
|
||||||
Map<String, Object> data = new HashMap<>();
|
// In your test, assuming the "preload.js" file is in the "mocks" directory.
|
||||||
data.put("text", "some data");
|
page.addInitScript(Paths.get("mocks/preload.js"));
|
||||||
data.put("value", 1);
|
|
||||||
Object result = page.evaluate("() => {\n" +
|
|
||||||
" // There is no |data| in the web page.\n" +
|
|
||||||
" window.myApp.use(data);\n" +
|
|
||||||
"}");
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```python async
|
```python async
|
||||||
data = { 'text': 'some data', 'value': 1 }
|
# In your test, assuming the "preload.js" file is in the "mocks" directory.
|
||||||
result = await page.evaluate("""() => {
|
await page.add_init_script(path="mocks/preload.js")
|
||||||
// There is no |data| in the web page.
|
|
||||||
window.myApp.use(data)
|
|
||||||
}""")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```python sync
|
```python sync
|
||||||
data = { 'text': 'some data', 'value': 1 }
|
# In your test, assuming the "preload.js" file is in the "mocks" directory.
|
||||||
result = page.evaluate("""() => {
|
page.add_init_script(path="mocks/preload.js")
|
||||||
// There is no |data| in the web page.
|
|
||||||
window.myApp.use(data)
|
|
||||||
}""")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var data = new { text = "some data", value = 1};
|
// In your test, assuming the "preload.js" file is in the "mocks" directory.
|
||||||
// Pass data as a parameter
|
await Page.AddInitScriptAsync(scriptPath: "mocks/preload.js");
|
||||||
var result = await page.EvaluateAsync(@"data => {
|
```
|
||||||
// There is no |data| in the web page.
|
|
||||||
window.myApp.use(data);
|
######
|
||||||
}");
|
* 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);
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
|
||||||
859
package-lock.json
generated
859
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -85,8 +85,8 @@
|
||||||
"eslint": "^8.55.0",
|
"eslint": "^8.55.0",
|
||||||
"eslint-plugin-internal-playwright": "file:utils/eslint-plugin-internal-playwright",
|
"eslint-plugin-internal-playwright": "file:utils/eslint-plugin-internal-playwright",
|
||||||
"eslint-plugin-notice": "^0.9.10",
|
"eslint-plugin-notice": "^0.9.10",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.35.0",
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"formidable": "^2.1.1",
|
"formidable": "^2.1.1",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
"mime": "^3.0.0",
|
"mime": "^3.0.0",
|
||||||
|
|
|
||||||
|
|
@ -70,19 +70,19 @@ export const blank = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const externalLink = () => {
|
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 = () => {
|
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 = () => {
|
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 = () => {
|
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 = () => {
|
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 && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
||||||
{!attachment.path && <span>{linkifyText(attachment.name)}</span>}
|
{!attachment.path && <span>{linkifyText(attachment.name)}</span>}
|
||||||
</span>} loadChildren={attachment.body ? () => {
|
</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>;
|
} : undefined} depth={0} style={{ lineHeight: '32px' }}></TreeItem>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export const TestCaseView: React.FC<{
|
||||||
{labels && <LabelsLinkView labels={labels} />}
|
{labels && <LabelsLinkView labels={labels} />}
|
||||||
</div>}
|
</div>}
|
||||||
{!!visibleAnnotations.length && <AutoChip header='Annotations'>
|
{!!visibleAnnotations.length && <AutoChip header='Annotations'>
|
||||||
{visibleAnnotations.map(annotation => <TestCaseAnnotationView annotation={annotation} />)}
|
{visibleAnnotations.map((annotation, index) => <TestCaseAnnotationView key={index} annotation={annotation} />)}
|
||||||
</AutoChip>}
|
</AutoChip>}
|
||||||
{test && <TabbedPane tabs={
|
{test && <TabbedPane tabs={
|
||||||
test.results.map((result, index) => ({
|
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",
|
"name": "chromium-tip-of-tree",
|
||||||
"revision": "1249",
|
"revision": "1250",
|
||||||
"installByDefault": false,
|
"installByDefault": false,
|
||||||
"browserVersion": "129.0.6654.0"
|
"browserVersion": "129.0.6658.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "firefox",
|
"name": "firefox",
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "webkit",
|
"name": "webkit",
|
||||||
"revision": "2061",
|
"revision": "2062",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"revisionOverrides": {
|
"revisionOverrides": {
|
||||||
"mac10.14": "1446",
|
"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> {
|
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 });
|
await this._channel.addInitScript({ source });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -552,13 +552,19 @@ function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
|
||||||
export async function toClientCertificatesProtocol(certs?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
|
export async function toClientCertificatesProtocol(certs?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
|
||||||
if (!certs)
|
if (!certs)
|
||||||
return undefined;
|
return undefined;
|
||||||
return await Promise.all(certs.map(async cert => {
|
|
||||||
return {
|
const bufferizeContent = async (value?: Buffer, path?: string): Promise<Buffer | undefined> => {
|
||||||
origin: cert.origin,
|
if (value)
|
||||||
cert: cert.certPath ? await fs.promises.readFile(cert.certPath) : undefined,
|
return value;
|
||||||
key: cert.keyPath ? await fs.promises.readFile(cert.keyPath) : undefined,
|
if (path)
|
||||||
pfx: cert.pfxPath ? await fs.promises.readFile(cert.pfxPath) : undefined,
|
return await fs.promises.readFile(path);
|
||||||
passphrase: cert.passphrase,
|
|
||||||
};
|
};
|
||||||
}));
|
|
||||||
|
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;
|
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') {
|
if (typeof fun === 'function') {
|
||||||
const source = fun.toString();
|
const source = fun.toString();
|
||||||
const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg);
|
const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg);
|
||||||
return `(${source})(${argString})`;
|
return `(${source})(${argString})`;
|
||||||
}
|
}
|
||||||
|
if (isString(fun)) {
|
||||||
if (arg !== undefined)
|
if (arg !== undefined)
|
||||||
throw new Error('Cannot evaluate a string with arguments');
|
throw new Error('Cannot evaluate a string with arguments');
|
||||||
if (isString(fun))
|
|
||||||
return fun;
|
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;
|
return fun.content;
|
||||||
|
}
|
||||||
if (fun.path !== undefined) {
|
if (fun.path !== undefined) {
|
||||||
let source = await fs.promises.readFile(fun.path, 'utf8');
|
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)
|
if (addSourceUrl)
|
||||||
source = addSourceUrlToScript(source, fun.path);
|
source = addSourceUrlToScript(source, fun.path);
|
||||||
return source;
|
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) {
|
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 });
|
await this._channel.addInitScript({ source });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export class Selectors implements api.Selectors {
|
||||||
private _registrations: channels.SelectorsRegisterParams[] = [];
|
private _registrations: channels.SelectorsRegisterParams[] = [];
|
||||||
|
|
||||||
async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> {
|
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 };
|
const params = { ...options, name, source };
|
||||||
for (const channel of this._channels)
|
for (const channel of this._channels)
|
||||||
await channel._channel.register(params);
|
await channel._channel.register(params);
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,11 @@ export const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domconten
|
||||||
|
|
||||||
export type ClientCertificate = {
|
export type ClientCertificate = {
|
||||||
origin: string;
|
origin: string;
|
||||||
|
cert?: Buffer;
|
||||||
certPath?: string;
|
certPath?: string;
|
||||||
|
key?: Buffer;
|
||||||
keyPath?: string;
|
keyPath?: string;
|
||||||
|
pfx?: Buffer;
|
||||||
pfxPath?: string;
|
pfxPath?: string;
|
||||||
passphrase?: string;
|
passphrase?: string;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ import { Tracing } from './trace/recorder/tracing';
|
||||||
import type * as types from './types';
|
import type * as types from './types';
|
||||||
import type { HeadersArray, ProxySettings } from './types';
|
import type { HeadersArray, ProxySettings } from './types';
|
||||||
import { kMaxCookieExpiresDateInSeconds } from './network';
|
import { kMaxCookieExpiresDateInSeconds } from './network';
|
||||||
import { clientCertificatesToTLSOptions, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
|
import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
|
||||||
|
|
||||||
type FetchRequestOptions = {
|
type FetchRequestOptions = {
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
|
|
@ -195,7 +195,7 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
maxRedirects: params.maxRedirects === 0 ? -1 : params.maxRedirects === undefined ? 20 : params.maxRedirects,
|
maxRedirects: params.maxRedirects === 0 ? -1 : params.maxRedirects === undefined ? 20 : params.maxRedirects,
|
||||||
timeout,
|
timeout,
|
||||||
deadline,
|
deadline,
|
||||||
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, requestUrl.origin),
|
...getMatchingTLSOptionsForOrigin(this._defaultOptions().clientCertificates, requestUrl.origin),
|
||||||
__testHookLookup: (params as any).__testHookLookup,
|
__testHookLookup: (params as any).__testHookLookup,
|
||||||
};
|
};
|
||||||
// rejectUnauthorized = undefined is treated as true in Node.js 12.
|
// rejectUnauthorized = undefined is treated as true in Node.js 12.
|
||||||
|
|
@ -365,7 +365,7 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
maxRedirects: options.maxRedirects - 1,
|
maxRedirects: options.maxRedirects - 1,
|
||||||
timeout: options.timeout,
|
timeout: options.timeout,
|
||||||
deadline: options.deadline,
|
deadline: options.deadline,
|
||||||
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, url.origin),
|
...getMatchingTLSOptionsForOrigin(this._defaultOptions().clientCertificates, url.origin),
|
||||||
__testHookLookup: options.__testHookLookup,
|
__testHookLookup: options.__testHookLookup,
|
||||||
};
|
};
|
||||||
// rejectUnauthorized = undefined is treated as true in node 12.
|
// rejectUnauthorized = undefined is treated as true in node 12.
|
||||||
|
|
|
||||||
|
|
@ -659,18 +659,24 @@ export class Frame extends SdkObject {
|
||||||
}
|
}
|
||||||
url = helper.completeUserURL(url);
|
url = helper.completeUserURL(url);
|
||||||
|
|
||||||
const sameDocument = helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (e: NavigationEvent) => !e.newDocument);
|
const navigationEvents: NavigationEvent[] = [];
|
||||||
const navigateResult = await this._page._delegate.navigateFrame(this, url, referer);
|
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;
|
let event: NavigationEvent;
|
||||||
if (navigateResult.newDocumentId) {
|
if (navigateResult.newDocumentId) {
|
||||||
sameDocument.dispose();
|
const predicate = (event: NavigationEvent) => {
|
||||||
event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (event: NavigationEvent) => {
|
|
||||||
// We are interested either in this specific document, or any other document that
|
// We are interested either in this specific document, or any other document that
|
||||||
// did commit and replaced the expected document.
|
// did commit and replaced the expected document.
|
||||||
return event.newDocument && (event.newDocument.documentId === navigateResult.newDocumentId || !event.error);
|
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) {
|
if (event.newDocument!.documentId !== navigateResult.newDocumentId) {
|
||||||
// This is just a sanity check. In practice, new navigation should
|
// This is just a sanity check. In practice, new navigation should
|
||||||
// cancel the previous one and report "request cancelled"-like error.
|
// cancel the previous one and report "request cancelled"-like error.
|
||||||
|
|
@ -679,7 +685,13 @@ export class Frame extends SdkObject {
|
||||||
if (event.error)
|
if (event.error)
|
||||||
throw event.error;
|
throw event.error;
|
||||||
} else {
|
} 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))
|
if (!this._firedLifecycleEvents.has(waitUntil))
|
||||||
|
|
|
||||||
|
|
@ -552,28 +552,14 @@ class TextAssertionTool implements RecorderTool {
|
||||||
private _recorder: Recorder;
|
private _recorder: Recorder;
|
||||||
private _hoverHighlight: HighlightModel | null = null;
|
private _hoverHighlight: HighlightModel | null = null;
|
||||||
private _action: actions.AssertAction | null = null;
|
private _action: actions.AssertAction | null = null;
|
||||||
private _dialogElement: HTMLElement | null = null;
|
private _dialog: Dialog;
|
||||||
private _acceptButton: HTMLElement;
|
|
||||||
private _cancelButton: HTMLElement;
|
|
||||||
private _keyboardListener: ((event: KeyboardEvent) => void) | undefined;
|
|
||||||
private _textCache = new Map<Element | ShadowRoot, ElementText>();
|
private _textCache = new Map<Element | ShadowRoot, ElementText>();
|
||||||
private _kind: 'text' | 'value';
|
private _kind: 'text' | 'value';
|
||||||
|
|
||||||
constructor(recorder: Recorder, kind: 'text' | 'value') {
|
constructor(recorder: Recorder, kind: 'text' | 'value') {
|
||||||
this._recorder = recorder;
|
this._recorder = recorder;
|
||||||
this._kind = kind;
|
this._kind = kind;
|
||||||
|
this._dialog = new Dialog(recorder);
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor() {
|
cursor() {
|
||||||
|
|
@ -581,7 +567,7 @@ class TextAssertionTool implements RecorderTool {
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
this._closeDialog();
|
this._dialog.close();
|
||||||
this._hoverHighlight = null;
|
this._hoverHighlight = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -590,7 +576,7 @@ class TextAssertionTool implements RecorderTool {
|
||||||
if (this._kind === 'value') {
|
if (this._kind === 'value') {
|
||||||
this._commitAssertValue();
|
this._commitAssertValue();
|
||||||
} else {
|
} else {
|
||||||
if (!this._dialogElement)
|
if (!this._dialog.isShowing())
|
||||||
this._showDialog();
|
this._showDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -611,7 +597,7 @@ class TextAssertionTool implements RecorderTool {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseMove(event: MouseEvent) {
|
onMouseMove(event: MouseEvent) {
|
||||||
if (this._dialogElement)
|
if (this._dialog.isShowing())
|
||||||
return;
|
return;
|
||||||
const target = this._recorder.deepEventTarget(event);
|
const target = this._recorder.deepEventTarget(event);
|
||||||
if (this._hoverHighlight?.elements[0] === target)
|
if (this._hoverHighlight?.elements[0] === target)
|
||||||
|
|
@ -691,9 +677,9 @@ class TextAssertionTool implements RecorderTool {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _commit() {
|
private _commit() {
|
||||||
if (!this._action || !this._dialogElement)
|
if (!this._action || !this._dialog.isShowing())
|
||||||
return;
|
return;
|
||||||
this._closeDialog();
|
this._dialog.close();
|
||||||
this._recorder.delegate.recordAction?.(this._action);
|
this._recorder.delegate.recordAction?.(this._action);
|
||||||
this._recorder.delegate.setMode?.('recording');
|
this._recorder.delegate.setMode?.('recording');
|
||||||
}
|
}
|
||||||
|
|
@ -705,31 +691,6 @@ class TextAssertionTool implements RecorderTool {
|
||||||
if (!this._action || this._action.name !== 'assertText')
|
if (!this._action || this._action.name !== 'assertText')
|
||||||
return;
|
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 action = this._action;
|
||||||
const textElement = this._recorder.document.createElement('textarea');
|
const textElement = this._recorder.document.createElement('textarea');
|
||||||
textElement.setAttribute('spellcheck', 'false');
|
textElement.setAttribute('spellcheck', 'false');
|
||||||
|
|
@ -747,24 +708,18 @@ class TextAssertionTool implements RecorderTool {
|
||||||
textElement.classList.toggle('does-not-match', !matches);
|
textElement.classList.toggle('does-not-match', !matches);
|
||||||
};
|
};
|
||||||
textElement.addEventListener('input', updateAndValidate);
|
textElement.addEventListener('input', updateAndValidate);
|
||||||
bodyElement.appendChild(textElement);
|
|
||||||
|
|
||||||
this._dialogElement.appendChild(bodyElement);
|
const label = 'Assert that element contains text';
|
||||||
this._recorder.highlight.appendChild(this._dialogElement);
|
const dialogElement = this._dialog.show({
|
||||||
const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, this._dialogElement);
|
label,
|
||||||
this._dialogElement.style.top = position.anchorTop + 'px';
|
body: textElement,
|
||||||
this._dialogElement.style.left = position.anchorLeft + 'px';
|
onCommit: () => this._commit(),
|
||||||
|
});
|
||||||
|
const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, dialogElement);
|
||||||
|
this._dialog.moveTo(position.anchorTop, position.anchorLeft);
|
||||||
textElement.focus();
|
textElement.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _closeDialog() {
|
|
||||||
if (!this._dialogElement)
|
|
||||||
return;
|
|
||||||
this._dialogElement.remove();
|
|
||||||
this._recorder.document.removeEventListener('keydown', this._keyboardListener!);
|
|
||||||
this._dialogElement = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _commitAssertValue() {
|
private _commitAssertValue() {
|
||||||
if (this._kind !== 'value')
|
if (this._kind !== 'value')
|
||||||
return;
|
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 {
|
function deepActiveElement(document: Document): Element | null {
|
||||||
let activeElement = document.activeElement;
|
let activeElement = document.activeElement;
|
||||||
while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement)
|
while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement)
|
||||||
|
|
|
||||||
|
|
@ -261,12 +261,16 @@ function getAriaBoolean(attr: string | null) {
|
||||||
return attr === null ? undefined : attr.toLowerCase() === 'true';
|
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
|
// https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion, but including "none" and "presentation" roles
|
||||||
// Not implemented:
|
// Not implemented:
|
||||||
// `Any descendants of elements that have the characteristic "Children Presentational: True"`
|
// `Any descendants of elements that have the characteristic "Children Presentational: True"`
|
||||||
// https://www.w3.org/TR/wai-aria-1.2/#aria-hidden
|
// https://www.w3.org/TR/wai-aria-1.2/#aria-hidden
|
||||||
export function isElementHiddenForAria(element: Element): boolean {
|
export function isElementHiddenForAria(element: Element): boolean {
|
||||||
if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element)))
|
if (isElementIgnoredForAria(element))
|
||||||
return true;
|
return true;
|
||||||
const style = getElementComputedStyle(element);
|
const style = getElementComputedStyle(element);
|
||||||
const isSlot = element.nodeName === 'SLOT';
|
const isSlot = element.nodeName === 'SLOT';
|
||||||
|
|
@ -371,7 +375,8 @@ function getPseudoContent(element: Element, pseudo: '::before' | '::after') {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPseudoContentImpl(pseudoStyle: CSSStyleDeclaration | undefined) {
|
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 '';
|
return '';
|
||||||
const content = pseudoStyle.content;
|
const content = pseudoStyle.content;
|
||||||
if ((content[0] === '\'' && content[content.length - 1] === '\'') ||
|
if ((content[0] === '\'' && content[content.length - 1] === '\'') ||
|
||||||
|
|
@ -496,15 +501,18 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
|
||||||
// step 2a. Hidden Not Referenced: If the current node is hidden and is:
|
// 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.
|
// 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.
|
// 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 &&
|
if (!options.includeHidden) {
|
||||||
!options.embeddedInLabelledBy?.hidden &&
|
const isEmbeddedInHiddenReferenceTraversal =
|
||||||
!options.embeddedInDescribedBy?.hidden &&
|
!!options.embeddedInLabelledBy?.hidden ||
|
||||||
!options?.embeddedInNativeTextAlternative?.hidden &&
|
!!options.embeddedInDescribedBy?.hidden ||
|
||||||
!options?.embeddedInLabel?.hidden &&
|
!!options.embeddedInNativeTextAlternative?.hidden ||
|
||||||
isElementHiddenForAria(element)) {
|
!!options.embeddedInLabel?.hidden;
|
||||||
|
if (isElementIgnoredForAria(element) ||
|
||||||
|
(!isEmbeddedInHiddenReferenceTraversal && isElementHiddenForAria(element))) {
|
||||||
options.visitedElements.add(element);
|
options.visitedElements.add(element);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const labelledBy = getAriaLabelledByElements(element);
|
const labelledBy = getAriaLabelledByElements(element);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
import path from 'path';
|
|
||||||
import http2 from 'http2';
|
import http2 from 'http2';
|
||||||
import type https from 'https';
|
import type https from 'https';
|
||||||
import fs from 'fs';
|
|
||||||
import tls from 'tls';
|
import tls from 'tls';
|
||||||
import stream from 'stream';
|
import stream from 'stream';
|
||||||
import { createSocket, createTLSSocket } from '../utils/happy-eyeballs';
|
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 type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../common/socksProxy';
|
||||||
import { SocksProxy } from '../common/socksProxy';
|
import { SocksProxy } from '../common/socksProxy';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
|
|
@ -32,10 +30,8 @@ let dummyServerTlsOptions: tls.TlsOptions | undefined = undefined;
|
||||||
function loadDummyServerCertsIfNeeded() {
|
function loadDummyServerCertsIfNeeded() {
|
||||||
if (dummyServerTlsOptions)
|
if (dummyServerTlsOptions)
|
||||||
return;
|
return;
|
||||||
dummyServerTlsOptions = {
|
const { cert, key } = generateSelfSignedCertificate();
|
||||||
key: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/key.pem')),
|
dummyServerTlsOptions = { key, cert };
|
||||||
cert: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/cert.pem')),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ALPNCache {
|
class ALPNCache {
|
||||||
|
|
@ -161,7 +157,6 @@ class SocksProxyConnection {
|
||||||
let targetTLS: tls.TLSSocket | undefined = undefined;
|
let targetTLS: tls.TLSSocket | undefined = undefined;
|
||||||
|
|
||||||
const handleError = (error: Error) => {
|
const handleError = (error: Error) => {
|
||||||
error = rewriteOpenSSLErrorIfNeeded(error);
|
|
||||||
debugLogger.log('client-certificates', `error when connecting to target: ${error.message.replaceAll('\n', ' ')}`);
|
debugLogger.log('client-certificates', `error when connecting to target: ${error.message.replaceAll('\n', ' ')}`);
|
||||||
const responseBody = escapeHTML('Playwright client-certificate error: ' + error.message)
|
const responseBody = escapeHTML('Playwright client-certificate error: ' + error.message)
|
||||||
.replaceAll('\n', ' <br>');
|
.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) {
|
if (this._closed) {
|
||||||
internalTLS.destroy();
|
internalTLS.destroy();
|
||||||
return;
|
return;
|
||||||
|
|
@ -221,7 +208,7 @@ class SocksProxyConnection {
|
||||||
rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors,
|
rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors,
|
||||||
ALPNProtocols: [internalTLS.alpnProtocol || 'http/1.1'],
|
ALPNProtocols: [internalTLS.alpnProtocol || 'http/1.1'],
|
||||||
servername: !net.isIP(this.host) ? this.host : undefined,
|
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', () => {
|
targetTLS.once('secureConnect', () => {
|
||||||
|
|
@ -240,7 +227,7 @@ export class ClientCertificatesProxy {
|
||||||
_socksProxy: SocksProxy;
|
_socksProxy: SocksProxy;
|
||||||
private _connections: Map<string, SocksProxyConnection> = new Map();
|
private _connections: Map<string, SocksProxyConnection> = new Map();
|
||||||
ignoreHTTPSErrors: boolean | undefined;
|
ignoreHTTPSErrors: boolean | undefined;
|
||||||
clientCertificates: channels.BrowserNewContextOptions['clientCertificates'];
|
secureContextMap: Map<string, tls.SecureContext> = new Map();
|
||||||
alpnCache: ALPNCache;
|
alpnCache: ALPNCache;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -248,7 +235,7 @@ export class ClientCertificatesProxy {
|
||||||
) {
|
) {
|
||||||
this.alpnCache = new ALPNCache();
|
this.alpnCache = new ALPNCache();
|
||||||
this.ignoreHTTPSErrors = contextOptions.ignoreHTTPSErrors;
|
this.ignoreHTTPSErrors = contextOptions.ignoreHTTPSErrors;
|
||||||
this.clientCertificates = contextOptions.clientCertificates;
|
this._initSecureContexts(contextOptions.clientCertificates);
|
||||||
this._socksProxy = new SocksProxy();
|
this._socksProxy = new SocksProxy();
|
||||||
this._socksProxy.setPattern('*');
|
this._socksProxy.setPattern('*');
|
||||||
this._socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => {
|
this._socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => {
|
||||||
|
|
@ -270,6 +257,27 @@ export class ClientCertificatesProxy {
|
||||||
loadDummyServerCertsIfNeeded();
|
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> {
|
public async listen(): Promise<string> {
|
||||||
const port = await this._socksProxy.listen(0, '127.0.0.1');
|
const port = await this._socksProxy.listen(0, '127.0.0.1');
|
||||||
return `socks5://127.0.0.1:${port}`;
|
return `socks5://127.0.0.1:${port}`;
|
||||||
|
|
@ -280,25 +288,25 @@ export class ClientCertificatesProxy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clientCertificatesToTLSOptions(
|
function normalizeOrigin(origin: string): string {
|
||||||
clientCertificates: channels.BrowserNewContextOptions['clientCertificates'],
|
|
||||||
origin: string
|
|
||||||
): Pick<https.RequestOptions, 'pfx' | 'key' | 'cert'> | undefined {
|
|
||||||
const matchingCerts = clientCertificates?.filter(c => {
|
|
||||||
try {
|
try {
|
||||||
return new URL(c.origin).origin === origin;
|
return new URL(origin).origin;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return c.origin === origin;
|
return origin;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
if (!matchingCerts || !matchingCerts.length)
|
|
||||||
|
function convertClientCertificatesToTLSOptions(
|
||||||
|
clientCertificates: channels.BrowserNewContextOptions['clientCertificates']
|
||||||
|
): Pick<https.RequestOptions, 'pfx' | 'key' | 'cert'> | undefined {
|
||||||
|
if (!clientCertificates || !clientCertificates.length)
|
||||||
return;
|
return;
|
||||||
const tlsOptions = {
|
const tlsOptions = {
|
||||||
pfx: [] as { buf: Buffer, passphrase?: string }[],
|
pfx: [] as { buf: Buffer, passphrase?: string }[],
|
||||||
key: [] as { pem: Buffer, passphrase?: string }[],
|
key: [] as { pem: Buffer, passphrase?: string }[],
|
||||||
cert: [] as Buffer[],
|
cert: [] as Buffer[],
|
||||||
};
|
};
|
||||||
for (const cert of matchingCerts) {
|
for (const cert of clientCertificates) {
|
||||||
if (cert.cert)
|
if (cert.cert)
|
||||||
tlsOptions.cert.push(cert.cert);
|
tlsOptions.cert.push(cert.cert);
|
||||||
if (cert.key)
|
if (cert.key)
|
||||||
|
|
@ -309,6 +317,16 @@ export function clientCertificatesToTLSOptions(
|
||||||
return tlsOptions;
|
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 {
|
function rewriteToLocalhostIfNeeded(host: string): string {
|
||||||
return host === 'local.playwright' ? 'localhost' : host;
|
return host === 'local.playwright' ? 'localhost' : host;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import { assert } from './debug';
|
||||||
|
|
||||||
export function createGuid(): string {
|
export function createGuid(): string {
|
||||||
return crypto.randomBytes(16).toString('hex');
|
return crypto.randomBytes(16).toString('hex');
|
||||||
|
|
@ -25,3 +26,170 @@ export function calculateSha1(buffer: Buffer | string): string {
|
||||||
hash.update(buffer);
|
hash.update(buffer);
|
||||||
return hash.digest('hex');
|
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)
|
* [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
|
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not
|
||||||
* defined.
|
* 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 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>;
|
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)
|
* [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
|
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not
|
||||||
* defined.
|
* 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 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>;
|
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<void>;
|
||||||
|
|
||||||
|
|
@ -9138,10 +9204,10 @@ export interface Browser {
|
||||||
*
|
*
|
||||||
* **Details**
|
* **Details**
|
||||||
*
|
*
|
||||||
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
* An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`,
|
||||||
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
* a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally,
|
||||||
* certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that
|
* `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided
|
||||||
* the certificate is valid for.
|
* 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.
|
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||||
*
|
*
|
||||||
|
|
@ -9159,16 +9225,31 @@ export interface Browser {
|
||||||
*/
|
*/
|
||||||
certPath?: string;
|
certPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direct value of the certificate in PEM format.
|
||||||
|
*/
|
||||||
|
cert?: Buffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to the file with the private key in PEM format.
|
* Path to the file with the private key in PEM format.
|
||||||
*/
|
*/
|
||||||
keyPath?: string;
|
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.
|
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||||
*/
|
*/
|
||||||
pfxPath?: string;
|
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).
|
* Passphrase for the private key (PEM or PFX).
|
||||||
*/
|
*/
|
||||||
|
|
@ -13850,10 +13931,10 @@ export interface BrowserType<Unused = {}> {
|
||||||
*
|
*
|
||||||
* **Details**
|
* **Details**
|
||||||
*
|
*
|
||||||
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
* An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`,
|
||||||
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
* a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally,
|
||||||
* certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that
|
* `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided
|
||||||
* the certificate is valid for.
|
* 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.
|
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||||
*
|
*
|
||||||
|
|
@ -13871,16 +13952,31 @@ export interface BrowserType<Unused = {}> {
|
||||||
*/
|
*/
|
||||||
certPath?: string;
|
certPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direct value of the certificate in PEM format.
|
||||||
|
*/
|
||||||
|
cert?: Buffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to the file with the private key in PEM format.
|
* Path to the file with the private key in PEM format.
|
||||||
*/
|
*/
|
||||||
keyPath?: string;
|
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.
|
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||||
*/
|
*/
|
||||||
pfxPath?: string;
|
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).
|
* Passphrase for the private key (PEM or PFX).
|
||||||
*/
|
*/
|
||||||
|
|
@ -16259,10 +16355,10 @@ export interface APIRequest {
|
||||||
*
|
*
|
||||||
* **Details**
|
* **Details**
|
||||||
*
|
*
|
||||||
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
* An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`,
|
||||||
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
* a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally,
|
||||||
* certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that
|
* `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided
|
||||||
* the certificate is valid for.
|
* 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.
|
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||||
*
|
*
|
||||||
|
|
@ -16280,16 +16376,31 @@ export interface APIRequest {
|
||||||
*/
|
*/
|
||||||
certPath?: string;
|
certPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direct value of the certificate in PEM format.
|
||||||
|
*/
|
||||||
|
cert?: Buffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to the file with the private key in PEM format.
|
* Path to the file with the private key in PEM format.
|
||||||
*/
|
*/
|
||||||
keyPath?: string;
|
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.
|
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||||
*/
|
*/
|
||||||
pfxPath?: string;
|
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).
|
* Passphrase for the private key (PEM or PFX).
|
||||||
*/
|
*/
|
||||||
|
|
@ -20600,10 +20711,10 @@ export interface BrowserContextOptions {
|
||||||
*
|
*
|
||||||
* **Details**
|
* **Details**
|
||||||
*
|
*
|
||||||
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
* An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`,
|
||||||
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
* a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally,
|
||||||
* certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that
|
* `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided
|
||||||
* the certificate is valid for.
|
* 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.
|
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||||
*
|
*
|
||||||
|
|
@ -20621,16 +20732,31 @@ export interface BrowserContextOptions {
|
||||||
*/
|
*/
|
||||||
certPath?: string;
|
certPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direct value of the certificate in PEM format.
|
||||||
|
*/
|
||||||
|
cert?: Buffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to the file with the private key in PEM format.
|
* Path to the file with the private key in PEM format.
|
||||||
*/
|
*/
|
||||||
keyPath?: string;
|
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.
|
* Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||||
*/
|
*/
|
||||||
pfxPath?: string;
|
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).
|
* 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<{
|
export const test: TestType<{
|
||||||
mount<HooksConfig>(
|
mount<HooksConfig>(
|
||||||
component: JSX.Element,
|
component: JSX.Element,
|
||||||
options: MountOptionsJsx<HooksConfig>
|
options?: MountOptionsJsx<HooksConfig>
|
||||||
): Promise<MountResultJsx>;
|
): Promise<MountResultJsx>;
|
||||||
mount<HooksConfig, Component = unknown>(
|
mount<HooksConfig, Component = unknown>(
|
||||||
component: Component,
|
component: Component,
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,11 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
}, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any],
|
}, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any],
|
||||||
|
|
||||||
_setupArtifacts: [async ({ playwright, screenshot }, use, testInfo) => {
|
_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);
|
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
|
||||||
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);
|
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);
|
||||||
const csiListener: ClientInstrumentationListener = {
|
const csiListener: ClientInstrumentationListener = {
|
||||||
|
|
@ -297,7 +302,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
clientInstrumentation.removeListener(csiListener);
|
clientInstrumentation.removeListener(csiListener);
|
||||||
await artifactsRecorder.didFinishTest();
|
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) => {
|
_contextFactory: [async ({ browser, video, _reuseContext, _combinedContextOptions /** mitigate dep-via-auto lack of traceability */ }, use, testInfo) => {
|
||||||
const testInfoImpl = testInfo as TestInfoImpl;
|
const testInfoImpl = testInfo as TestInfoImpl;
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export class TestRun {
|
||||||
export function createTaskRunner(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner<TestRun> {
|
export function createTaskRunner(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner<TestRun> {
|
||||||
const taskRunner = TaskRunner.create<TestRun>(reporters, config.config.globalTimeout);
|
const taskRunner = TaskRunner.create<TestRun>(reporters, config.config.globalTimeout);
|
||||||
addGlobalSetupTasks(taskRunner, config);
|
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);
|
addRunTasks(taskRunner, config);
|
||||||
return taskRunner;
|
return taskRunner;
|
||||||
}
|
}
|
||||||
|
|
@ -76,14 +76,14 @@ export function createTaskRunnerForWatchSetup(config: FullConfigInternal, report
|
||||||
|
|
||||||
export function createTaskRunnerForWatch(config: FullConfigInternal, reporters: ReporterV2[], additionalFileMatcher?: Matcher): TaskRunner<TestRun> {
|
export function createTaskRunnerForWatch(config: FullConfigInternal, reporters: ReporterV2[], additionalFileMatcher?: Matcher): TaskRunner<TestRun> {
|
||||||
const taskRunner = TaskRunner.create<TestRun>(reporters);
|
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);
|
addRunTasks(taskRunner, config);
|
||||||
return taskRunner;
|
return taskRunner;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTaskRunnerForTestServer(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner<TestRun> {
|
export function createTaskRunnerForTestServer(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner<TestRun> {
|
||||||
const taskRunner = TaskRunner.create<TestRun>(reporters);
|
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);
|
addRunTasks(taskRunner, config);
|
||||||
return taskRunner;
|
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> {
|
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);
|
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());
|
taskRunner.addTask('report begin', createReportBeginTask());
|
||||||
return taskRunner;
|
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 {
|
return {
|
||||||
setup: async (reporter, testRun, errors, softErrors) => {
|
setup: async (reporter, testRun, errors, softErrors) => {
|
||||||
await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter, options.additionalFileMatcher);
|
await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter, options.additionalFileMatcher);
|
||||||
await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors);
|
await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors);
|
||||||
|
|
||||||
let cliOnlyChangedMatcher: Matcher | undefined = undefined;
|
let cliOnlyChangedMatcher: Matcher | undefined = undefined;
|
||||||
if (testRun.config.cliOnlyChanged && options.filterOnlyChanged) {
|
if (testRun.config.cliOnlyChanged) {
|
||||||
for (const plugin of testRun.config.plugins)
|
for (const plugin of testRun.config.plugins)
|
||||||
await plugin.instance?.populateDependencies?.();
|
await plugin.instance?.populateDependencies?.();
|
||||||
const changedFiles = await detectChangedTestFiles(testRun.config.cliOnlyChanged, testRun.config.configDir);
|
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**
|
* **Details**
|
||||||
*
|
*
|
||||||
* An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a
|
* An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`,
|
||||||
* single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the
|
* a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally,
|
||||||
* certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that
|
* `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided
|
||||||
* the certificate is valid for.
|
* 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.
|
* **NOTE** Using Client Certificates in combination with Proxy Servers is not supported.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ export const Recorder: React.FC<RecorderProps> = ({
|
||||||
sidebarSize={200}
|
sidebarSize={200}
|
||||||
main={<CodeMirrorWrapper text={source.text} language={source.language} highlight={source.highlight} revealLine={source.revealLine} readOnly={true} lineNumbers={true} />}
|
main={<CodeMirrorWrapper text={source.text} language={source.language} highlight={source.highlight} revealLine={source.revealLine} readOnly={true} lineNumbers={true} />}
|
||||||
sidebar={<TabbedPane
|
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={[
|
tabs={[
|
||||||
{
|
{
|
||||||
id: 'locator',
|
id: 'locator',
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,8 @@ export class SnapshotServer {
|
||||||
contentType = `${contentType}; charset=utf-8`;
|
contentType = `${contentType}; charset=utf-8`;
|
||||||
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
|
// "x-unknown" in the har means "no content type".
|
||||||
|
if (contentType !== 'x-unknown')
|
||||||
headers.set('Content-Type', contentType);
|
headers.set('Content-Type', contentType);
|
||||||
for (const { name, value } of resource.response.headers)
|
for (const { name, value } of resource.response.headers)
|
||||||
headers.set(name, value);
|
headers.set(name, value);
|
||||||
|
|
|
||||||
|
|
@ -114,9 +114,11 @@ export class TraceModel {
|
||||||
|
|
||||||
async resourceForSha1(sha1: string): Promise<Blob | undefined> {
|
async resourceForSha1(sha1: string): Promise<Blob | undefined> {
|
||||||
const blob = await this._backend.readBlob('resources/' + sha1);
|
const blob = await this._backend.readBlob('resources/' + sha1);
|
||||||
if (!blob)
|
const contentType = this._resourceToContentType.get(sha1);
|
||||||
return;
|
// "x-unknown" in the har means "no content type".
|
||||||
return new Blob([blob], { type: this._resourceToContentType.get(sha1) || 'application/octet-stream' });
|
if (!blob || contentType === undefined || contentType === 'x-unknown')
|
||||||
|
return blob;
|
||||||
|
return new Blob([blob], { type: contentType });
|
||||||
}
|
}
|
||||||
|
|
||||||
storage(): SnapshotStorage {
|
storage(): SnapshotStorage {
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ export const AttachmentsTab: React.FunctionComponent<{
|
||||||
const url = attachmentURL(a);
|
const url = attachmentURL(a);
|
||||||
return <div className='attachment-item' key={`screenshot-${i}`}>
|
return <div className='attachment-item' key={`screenshot-${i}`}>
|
||||||
<div><img draggable='false' src={url} /></div>
|
<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>;
|
</div>;
|
||||||
})}
|
})}
|
||||||
{attachments.size ? <div className='attachments-section'>Attachments</div> : undefined}
|
{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[] {
|
function formatAnsi(text: string): JSX.Element[] {
|
||||||
|
// eslint-disable-next-line react/jsx-key
|
||||||
return [<span dangerouslySetInnerHTML={{ __html: ansi2html(text.trim()) }}></span>];
|
return [<span dangerouslySetInnerHTML={{ __html: ansi2html(text.trim()) }}></span>];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,10 @@ export type FilterState = {
|
||||||
|
|
||||||
export const defaultFilterState: FilterState = { searchValue: '', resourceType: 'All' };
|
export const defaultFilterState: FilterState = { searchValue: '', resourceType: 'All' };
|
||||||
|
|
||||||
export const NetworkFilters: React.FunctionComponent<{
|
export const NetworkFilters = ({ filterState, onFilterStateChange }: {
|
||||||
filterState: FilterState,
|
filterState: FilterState,
|
||||||
onFilterStateChange: (filterState: FilterState) => void,
|
onFilterStateChange: (filterState: FilterState) => void,
|
||||||
}> = ({ filterState, onFilterStateChange }) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className='network-filters'>
|
<div className='network-filters'>
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,22 @@
|
||||||
overflow: hidden;
|
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 {
|
.tab-network .toolbar {
|
||||||
min-height: 30px !important;
|
min-height: 30px !important;
|
||||||
background-color: initial !important;
|
background-color: initial !important;
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
||||||
|
|
||||||
return <TabbedPane
|
return <TabbedPane
|
||||||
dataTestId='network-request-details'
|
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={[
|
tabs={[
|
||||||
{
|
{
|
||||||
id: 'request',
|
id: 'request',
|
||||||
|
|
@ -101,12 +101,13 @@ const ResponseTab: React.FunctionComponent<{
|
||||||
const BodyTab: React.FunctionComponent<{
|
const BodyTab: React.FunctionComponent<{
|
||||||
resource: ResourceSnapshot;
|
resource: ResourceSnapshot;
|
||||||
}> = ({ resource }) => {
|
}> = ({ 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(() => {
|
React.useEffect(() => {
|
||||||
const readResources = async () => {
|
const readResources = async () => {
|
||||||
if (resource.response.content._sha1) {
|
if (resource.response.content._sha1) {
|
||||||
const useBase64 = resource.response.content.mimeType.includes('image');
|
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}`);
|
const response = await fetch(`sha1/${resource.response.content._sha1}`);
|
||||||
if (useBase64) {
|
if (useBase64) {
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
|
|
@ -114,6 +115,9 @@ const BodyTab: React.FunctionComponent<{
|
||||||
const eventPromise = new Promise<any>(f => reader.onload = f);
|
const eventPromise = new Promise<any>(f => reader.onload = f);
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
setResponseBody({ dataUrl: (await eventPromise).target.result });
|
setResponseBody({ dataUrl: (await eventPromise).target.result });
|
||||||
|
} else if (isFont) {
|
||||||
|
const font = await response.arrayBuffer();
|
||||||
|
setResponseBody({ font });
|
||||||
} else {
|
} else {
|
||||||
const formattedBody = formatBody(await response.text(), resource.response.content.mimeType);
|
const formattedBody = formatBody(await response.text(), resource.response.content.mimeType);
|
||||||
setResponseBody({ text: formattedBody, mimeType: 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'>
|
return <div className='network-request-details-tab'>
|
||||||
{!resource.response.content._sha1 && <div>Response body is not available for this request.</div>}
|
{!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.dataUrl && <img draggable='false' src={responseBody.dataUrl} />}
|
||||||
{responseBody && responseBody.text && <CodeMirrorWrapper text={responseBody.text} mimeType={responseBody.mimeType} readOnly lineNumbers={true}/>}
|
{responseBody && responseBody.text && <CodeMirrorWrapper text={responseBody.text} mimeType={responseBody.mimeType} readOnly lineNumbers={true}/>}
|
||||||
</div>;
|
</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 {
|
function statusClass(statusCode: number): string {
|
||||||
if (statusCode < 300 || statusCode === 304)
|
if (statusCode < 300 || statusCode === 304)
|
||||||
return 'green-circle';
|
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)} />
|
<ToolbarButton className='pick-locator' title='Pick locator' icon='target' toggled={isInspecting} onClick={() => setIsInspecting(!isInspecting)} />
|
||||||
{['action', 'before', 'after'].map(tab => {
|
{['action', 'before', 'after'].map(tab => {
|
||||||
return <TabbedPaneTab
|
return <TabbedPaneTab
|
||||||
|
key={tab}
|
||||||
id={tab}
|
id={tab}
|
||||||
title={renderTitle(tab)}
|
title={renderTitle(tab)}
|
||||||
selected={snapshotTab === tab}
|
selected={snapshotTab === tab}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
import { clsx } from '@web/uiUtils';
|
import { clsx } from '@web/uiUtils';
|
||||||
import './tag.css';
|
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
|
return <span
|
||||||
className={clsx('tag', `tag-color-${tagNameToColor(tag)}`)}
|
className={clsx('tag', `tag-color-${tagNameToColor(tag)}`)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ export const FiltersView: React.FC<{
|
||||||
{expanded && <div className='hbox' style={{ marginLeft: 14, maxHeight: 200, overflowY: 'auto' }}>
|
{expanded && <div className='hbox' style={{ marginLeft: 14, maxHeight: 200, overflowY: 'auto' }}>
|
||||||
<div className='filter-list'>
|
<div className='filter-list'>
|
||||||
{[...statusFilters.entries()].map(([status, value]) => {
|
{[...statusFilters.entries()].map(([status, value]) => {
|
||||||
return <div className='filter-entry'>
|
return <div className='filter-entry' key={status}>
|
||||||
<label>
|
<label>
|
||||||
<input type='checkbox' checked={value} onClick={() => {
|
<input type='checkbox' checked={value} onClick={() => {
|
||||||
const copy = new Map(statusFilters);
|
const copy = new Map(statusFilters);
|
||||||
|
|
@ -74,7 +74,7 @@ export const FiltersView: React.FC<{
|
||||||
</div>
|
</div>
|
||||||
<div className='filter-list'>
|
<div className='filter-list'>
|
||||||
{[...projectFilters.entries()].map(([projectName, value]) => {
|
{[...projectFilters.entries()].map(([projectName, value]) => {
|
||||||
return <div className='filter-entry'>
|
return <div className='filter-entry' key={projectName}>
|
||||||
<label>
|
<label>
|
||||||
<input type='checkbox' checked={value} onClick={() => {
|
<input type='checkbox' checked={value} onClick={() => {
|
||||||
const copy = new Map(projectFilters);
|
const copy = new Map(projectFilters);
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ export function GridView<T>(model: GridViewProps<T>) {
|
||||||
<div className='grid-view-header'>
|
<div className='grid-view-header'>
|
||||||
{model.columns.map((column, i) => {
|
{model.columns.map((column, i) => {
|
||||||
return <div
|
return <div
|
||||||
|
key={model.columnTitle(column)}
|
||||||
className={'grid-view-header-cell ' + sortingHeader(column, model.sorting)}
|
className={'grid-view-header-cell ' + sortingHeader(column, model.sorting)}
|
||||||
style={{
|
style={{
|
||||||
width: i < model.columns.length - 1 ? model.columnWidths.get(column) : undefined,
|
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) => {
|
{model.columns.map((column, i) => {
|
||||||
const { body, title } = model.render(item, column, index);
|
const { body, title } = model.render(item, column, index);
|
||||||
return <div
|
return <div
|
||||||
|
key={model.columnTitle(column)}
|
||||||
className={`grid-view-cell grid-view-column-${String(column)}`}
|
className={`grid-view-cell grid-view-column-${String(column)}`}
|
||||||
title={title}
|
title={title}
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,7 @@ export function ListView<T>({
|
||||||
onMouseEnter={() => setHighlightedItem(item)}
|
onMouseEnter={() => setHighlightedItem(item)}
|
||||||
onMouseLeave={() => setHighlightedItem(undefined)}
|
onMouseLeave={() => setHighlightedItem(undefined)}
|
||||||
>
|
>
|
||||||
|
{/* eslint-disable-next-line react/jsx-key */}
|
||||||
{indentation ? new Array(indentation).fill(0).map(() => <div className='list-view-indent'></div>) : undefined}
|
{indentation ? new Array(indentation).fill(0).map(() => <div className='list-view-indent'></div>) : undefined}
|
||||||
{icon && <div
|
{icon && <div
|
||||||
className={'codicon ' + (icon(item, index) || 'codicon-blank')}
|
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' }}>
|
{mode === 'default' && <div style={{ flex: 'auto', display: 'flex', height: '100%', overflow: 'hidden' }}>
|
||||||
{[...tabs.map(tab => (
|
{[...tabs.map(tab => (
|
||||||
<TabbedPaneTab
|
<TabbedPaneTab
|
||||||
|
key={tab.id}
|
||||||
id={tab.id}
|
id={tab.id}
|
||||||
title={tab.title}
|
title={tab.title}
|
||||||
count={tab.count}
|
count={tab.count}
|
||||||
|
|
@ -67,7 +68,7 @@ export const TabbedPane: React.FunctionComponent<{
|
||||||
suffix = ` (${tab.count})`;
|
suffix = ` (${tab.count})`;
|
||||||
if (tab.errorCount)
|
if (tab.errorCount)
|
||||||
suffix = ` (${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>
|
</select>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
|
||||||
title={title}
|
title={title}
|
||||||
disabled={!!disabled}
|
disabled={!!disabled}
|
||||||
style={style}
|
style={style}
|
||||||
data-testId={testId}
|
data-testid={testId}
|
||||||
>
|
>
|
||||||
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
|
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -119,9 +119,9 @@ export const ImageDiffView: React.FC<{
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ alignSelf: 'start', lineHeight: '18px', marginLeft: '15px' }}>
|
<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>{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}>{diff.actual!.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}>{diff.expected!.attachment.name}</a></div>
|
<div><a target={noTargetBlank ? '' : '_blank'} href={diff.expected!.attachment.path} rel='noreferrer'>{diff.expected!.attachment.name}</a></div>
|
||||||
</div>
|
</div>
|
||||||
</>}
|
</>}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ export const ResizeView: React.FC<{
|
||||||
/>}
|
/>}
|
||||||
{offsets.map((offset, index) => {
|
{offsets.map((offset, index) => {
|
||||||
return <div
|
return <div
|
||||||
|
key={index}
|
||||||
style={{
|
style={{
|
||||||
...dividerStyle,
|
...dividerStyle,
|
||||||
top: orientation === 'horizontal' ? 0 : offset,
|
top: orientation === 'horizontal' ? 0 : offset,
|
||||||
|
|
|
||||||
74
packages/web/src/third_party/vscode/codicon.css
vendored
74
packages/web/src/third_party/vscode/codicon.css
vendored
|
|
@ -23,7 +23,6 @@
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.codicon-blank:before { content: '\2003'; }
|
|
||||||
.codicon-add:before { content: '\ea60'; }
|
.codicon-add:before { content: '\ea60'; }
|
||||||
.codicon-plus:before { content: '\ea60'; }
|
.codicon-plus:before { content: '\ea60'; }
|
||||||
.codicon-gist-new:before { content: '\ea60'; }
|
.codicon-gist-new:before { content: '\ea60'; }
|
||||||
|
|
@ -39,6 +38,7 @@
|
||||||
.codicon-record-keys:before { content: '\ea65'; }
|
.codicon-record-keys:before { content: '\ea65'; }
|
||||||
.codicon-keyboard:before { content: '\ea65'; }
|
.codicon-keyboard:before { content: '\ea65'; }
|
||||||
.codicon-tag:before { content: '\ea66'; }
|
.codicon-tag:before { content: '\ea66'; }
|
||||||
|
.codicon-git-pull-request-label:before { content: '\ea66'; }
|
||||||
.codicon-tag-add:before { content: '\ea66'; }
|
.codicon-tag-add:before { content: '\ea66'; }
|
||||||
.codicon-tag-remove:before { content: '\ea66'; }
|
.codicon-tag-remove:before { content: '\ea66'; }
|
||||||
.codicon-person:before { content: '\ea67'; }
|
.codicon-person:before { content: '\ea67'; }
|
||||||
|
|
@ -74,6 +74,7 @@
|
||||||
.codicon-debug-breakpoint:before { content: '\ea71'; }
|
.codicon-debug-breakpoint:before { content: '\ea71'; }
|
||||||
.codicon-debug-breakpoint-disabled:before { content: '\ea71'; }
|
.codicon-debug-breakpoint-disabled:before { content: '\ea71'; }
|
||||||
.codicon-debug-hint:before { content: '\ea71'; }
|
.codicon-debug-hint:before { content: '\ea71'; }
|
||||||
|
.codicon-terminal-decoration-success:before { content: '\ea71'; }
|
||||||
.codicon-primitive-square:before { content: '\ea72'; }
|
.codicon-primitive-square:before { content: '\ea72'; }
|
||||||
.codicon-edit:before { content: '\ea73'; }
|
.codicon-edit:before { content: '\ea73'; }
|
||||||
.codicon-pencil:before { content: '\ea73'; }
|
.codicon-pencil:before { content: '\ea73'; }
|
||||||
|
|
@ -185,7 +186,6 @@
|
||||||
.codicon-check:before { content: '\eab2'; }
|
.codicon-check:before { content: '\eab2'; }
|
||||||
.codicon-checklist:before { content: '\eab3'; }
|
.codicon-checklist:before { content: '\eab3'; }
|
||||||
.codicon-chevron-down:before { content: '\eab4'; }
|
.codicon-chevron-down:before { content: '\eab4'; }
|
||||||
.codicon-drop-down-button:before { content: '\eab4'; }
|
|
||||||
.codicon-chevron-left:before { content: '\eab5'; }
|
.codicon-chevron-left:before { content: '\eab5'; }
|
||||||
.codicon-chevron-right:before { content: '\eab6'; }
|
.codicon-chevron-right:before { content: '\eab6'; }
|
||||||
.codicon-chevron-up:before { content: '\eab7'; }
|
.codicon-chevron-up:before { content: '\eab7'; }
|
||||||
|
|
@ -193,9 +193,10 @@
|
||||||
.codicon-chrome-maximize:before { content: '\eab9'; }
|
.codicon-chrome-maximize:before { content: '\eab9'; }
|
||||||
.codicon-chrome-minimize:before { content: '\eaba'; }
|
.codicon-chrome-minimize:before { content: '\eaba'; }
|
||||||
.codicon-chrome-restore:before { content: '\eabb'; }
|
.codicon-chrome-restore:before { content: '\eabb'; }
|
||||||
.codicon-circle:before { content: '\eabc'; }
|
|
||||||
.codicon-circle-outline:before { content: '\eabc'; }
|
.codicon-circle-outline:before { content: '\eabc'; }
|
||||||
|
.codicon-circle:before { content: '\eabc'; }
|
||||||
.codicon-debug-breakpoint-unverified:before { content: '\eabc'; }
|
.codicon-debug-breakpoint-unverified:before { content: '\eabc'; }
|
||||||
|
.codicon-terminal-decoration-incomplete:before { content: '\eabc'; }
|
||||||
.codicon-circle-slash:before { content: '\eabd'; }
|
.codicon-circle-slash:before { content: '\eabd'; }
|
||||||
.codicon-circuit-board:before { content: '\eabe'; }
|
.codicon-circuit-board:before { content: '\eabe'; }
|
||||||
.codicon-clear-all:before { content: '\eabf'; }
|
.codicon-clear-all:before { content: '\eabf'; }
|
||||||
|
|
@ -207,7 +208,6 @@
|
||||||
.codicon-collapse-all:before { content: '\eac5'; }
|
.codicon-collapse-all:before { content: '\eac5'; }
|
||||||
.codicon-color-mode:before { content: '\eac6'; }
|
.codicon-color-mode:before { content: '\eac6'; }
|
||||||
.codicon-comment-discussion:before { content: '\eac7'; }
|
.codicon-comment-discussion:before { content: '\eac7'; }
|
||||||
.codicon-compare-changes:before { content: '\eafd'; }
|
|
||||||
.codicon-credit-card:before { content: '\eac9'; }
|
.codicon-credit-card:before { content: '\eac9'; }
|
||||||
.codicon-dash:before { content: '\eacc'; }
|
.codicon-dash:before { content: '\eacc'; }
|
||||||
.codicon-dashboard:before { content: '\eacd'; }
|
.codicon-dashboard:before { content: '\eacd'; }
|
||||||
|
|
@ -231,6 +231,7 @@
|
||||||
.codicon-diff-removed:before { content: '\eadf'; }
|
.codicon-diff-removed:before { content: '\eadf'; }
|
||||||
.codicon-diff-renamed:before { content: '\eae0'; }
|
.codicon-diff-renamed:before { content: '\eae0'; }
|
||||||
.codicon-diff:before { content: '\eae1'; }
|
.codicon-diff:before { content: '\eae1'; }
|
||||||
|
.codicon-diff-sidebyside:before { content: '\eae1'; }
|
||||||
.codicon-discard:before { content: '\eae2'; }
|
.codicon-discard:before { content: '\eae2'; }
|
||||||
.codicon-editor-layout:before { content: '\eae3'; }
|
.codicon-editor-layout:before { content: '\eae3'; }
|
||||||
.codicon-empty-window:before { content: '\eae4'; }
|
.codicon-empty-window:before { content: '\eae4'; }
|
||||||
|
|
@ -259,6 +260,7 @@
|
||||||
.codicon-gist:before { content: '\eafb'; }
|
.codicon-gist:before { content: '\eafb'; }
|
||||||
.codicon-git-commit:before { content: '\eafc'; }
|
.codicon-git-commit:before { content: '\eafc'; }
|
||||||
.codicon-git-compare:before { content: '\eafd'; }
|
.codicon-git-compare:before { content: '\eafd'; }
|
||||||
|
.codicon-compare-changes:before { content: '\eafd'; }
|
||||||
.codicon-git-merge:before { content: '\eafe'; }
|
.codicon-git-merge:before { content: '\eafe'; }
|
||||||
.codicon-github-action:before { content: '\eaff'; }
|
.codicon-github-action:before { content: '\eaff'; }
|
||||||
.codicon-github-alt:before { content: '\eb00'; }
|
.codicon-github-alt:before { content: '\eb00'; }
|
||||||
|
|
@ -271,13 +273,11 @@
|
||||||
.codicon-horizontal-rule:before { content: '\eb07'; }
|
.codicon-horizontal-rule:before { content: '\eb07'; }
|
||||||
.codicon-hubot:before { content: '\eb08'; }
|
.codicon-hubot:before { content: '\eb08'; }
|
||||||
.codicon-inbox:before { content: '\eb09'; }
|
.codicon-inbox:before { content: '\eb09'; }
|
||||||
.codicon-issue-closed:before { content: '\eba4'; }
|
|
||||||
.codicon-issue-reopened:before { content: '\eb0b'; }
|
.codicon-issue-reopened:before { content: '\eb0b'; }
|
||||||
.codicon-issues:before { content: '\eb0c'; }
|
.codicon-issues:before { content: '\eb0c'; }
|
||||||
.codicon-italic:before { content: '\eb0d'; }
|
.codicon-italic:before { content: '\eb0d'; }
|
||||||
.codicon-jersey:before { content: '\eb0e'; }
|
.codicon-jersey:before { content: '\eb0e'; }
|
||||||
.codicon-json:before { content: '\eb0f'; }
|
.codicon-json:before { content: '\eb0f'; }
|
||||||
.codicon-bracket:before { content: '\eb0f'; }
|
|
||||||
.codicon-kebab-vertical:before { content: '\eb10'; }
|
.codicon-kebab-vertical:before { content: '\eb10'; }
|
||||||
.codicon-key:before { content: '\eb11'; }
|
.codicon-key:before { content: '\eb11'; }
|
||||||
.codicon-law:before { content: '\eb12'; }
|
.codicon-law:before { content: '\eb12'; }
|
||||||
|
|
@ -295,6 +295,7 @@
|
||||||
.codicon-megaphone:before { content: '\eb1e'; }
|
.codicon-megaphone:before { content: '\eb1e'; }
|
||||||
.codicon-mention:before { content: '\eb1f'; }
|
.codicon-mention:before { content: '\eb1f'; }
|
||||||
.codicon-milestone:before { content: '\eb20'; }
|
.codicon-milestone:before { content: '\eb20'; }
|
||||||
|
.codicon-git-pull-request-milestone:before { content: '\eb20'; }
|
||||||
.codicon-mortar-board:before { content: '\eb21'; }
|
.codicon-mortar-board:before { content: '\eb21'; }
|
||||||
.codicon-move:before { content: '\eb22'; }
|
.codicon-move:before { content: '\eb22'; }
|
||||||
.codicon-multiple-windows:before { content: '\eb23'; }
|
.codicon-multiple-windows:before { content: '\eb23'; }
|
||||||
|
|
@ -355,7 +356,6 @@
|
||||||
.codicon-star-half:before { content: '\eb5a'; }
|
.codicon-star-half:before { content: '\eb5a'; }
|
||||||
.codicon-symbol-class:before { content: '\eb5b'; }
|
.codicon-symbol-class:before { content: '\eb5b'; }
|
||||||
.codicon-symbol-color:before { content: '\eb5c'; }
|
.codicon-symbol-color:before { content: '\eb5c'; }
|
||||||
.codicon-symbol-customcolor:before { content: '\eb5c'; }
|
|
||||||
.codicon-symbol-constant:before { content: '\eb5d'; }
|
.codicon-symbol-constant:before { content: '\eb5d'; }
|
||||||
.codicon-symbol-enum-member:before { content: '\eb5e'; }
|
.codicon-symbol-enum-member:before { content: '\eb5e'; }
|
||||||
.codicon-symbol-field:before { content: '\eb5f'; }
|
.codicon-symbol-field:before { content: '\eb5f'; }
|
||||||
|
|
@ -407,6 +407,7 @@
|
||||||
.codicon-debug-stackframe-active:before { content: '\eb89'; }
|
.codicon-debug-stackframe-active:before { content: '\eb89'; }
|
||||||
.codicon-circle-small-filled:before { content: '\eb8a'; }
|
.codicon-circle-small-filled:before { content: '\eb8a'; }
|
||||||
.codicon-debug-stackframe-dot: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:before { content: '\eb8b'; }
|
||||||
.codicon-debug-stackframe-focused:before { content: '\eb8b'; }
|
.codicon-debug-stackframe-focused:before { content: '\eb8b'; }
|
||||||
.codicon-debug-breakpoint-unsupported:before { content: '\eb8c'; }
|
.codicon-debug-breakpoint-unsupported:before { content: '\eb8c'; }
|
||||||
|
|
@ -414,14 +415,17 @@
|
||||||
.codicon-debug-reverse-continue:before { content: '\eb8e'; }
|
.codicon-debug-reverse-continue:before { content: '\eb8e'; }
|
||||||
.codicon-debug-step-back:before { content: '\eb8f'; }
|
.codicon-debug-step-back:before { content: '\eb8f'; }
|
||||||
.codicon-debug-restart-frame:before { content: '\eb90'; }
|
.codicon-debug-restart-frame:before { content: '\eb90'; }
|
||||||
|
.codicon-debug-alt:before { content: '\eb91'; }
|
||||||
.codicon-call-incoming:before { content: '\eb92'; }
|
.codicon-call-incoming:before { content: '\eb92'; }
|
||||||
.codicon-call-outgoing:before { content: '\eb93'; }
|
.codicon-call-outgoing:before { content: '\eb93'; }
|
||||||
.codicon-menu:before { content: '\eb94'; }
|
.codicon-menu:before { content: '\eb94'; }
|
||||||
.codicon-expand-all:before { content: '\eb95'; }
|
.codicon-expand-all:before { content: '\eb95'; }
|
||||||
.codicon-feedback:before { content: '\eb96'; }
|
.codicon-feedback:before { content: '\eb96'; }
|
||||||
|
.codicon-git-pull-request-reviewer:before { content: '\eb96'; }
|
||||||
.codicon-group-by-ref-type:before { content: '\eb97'; }
|
.codicon-group-by-ref-type:before { content: '\eb97'; }
|
||||||
.codicon-ungroup-by-ref-type:before { content: '\eb98'; }
|
.codicon-ungroup-by-ref-type:before { content: '\eb98'; }
|
||||||
.codicon-account:before { content: '\eb99'; }
|
.codicon-account:before { content: '\eb99'; }
|
||||||
|
.codicon-git-pull-request-assignee:before { content: '\eb99'; }
|
||||||
.codicon-bell-dot:before { content: '\eb9a'; }
|
.codicon-bell-dot:before { content: '\eb9a'; }
|
||||||
.codicon-debug-console:before { content: '\eb9b'; }
|
.codicon-debug-console:before { content: '\eb9b'; }
|
||||||
.codicon-library:before { content: '\eb9c'; }
|
.codicon-library:before { content: '\eb9c'; }
|
||||||
|
|
@ -430,10 +434,10 @@
|
||||||
.codicon-sync-ignored:before { content: '\eb9f'; }
|
.codicon-sync-ignored:before { content: '\eb9f'; }
|
||||||
.codicon-pinned:before { content: '\eba0'; }
|
.codicon-pinned:before { content: '\eba0'; }
|
||||||
.codicon-github-inverted:before { content: '\eba1'; }
|
.codicon-github-inverted:before { content: '\eba1'; }
|
||||||
.codicon-debug-alt:before { content: '\eb91'; }
|
|
||||||
.codicon-server-process:before { content: '\eba2'; }
|
.codicon-server-process:before { content: '\eba2'; }
|
||||||
.codicon-server-environment:before { content: '\eba3'; }
|
.codicon-server-environment:before { content: '\eba3'; }
|
||||||
.codicon-pass:before { content: '\eba4'; }
|
.codicon-pass:before { content: '\eba4'; }
|
||||||
|
.codicon-issue-closed:before { content: '\eba4'; }
|
||||||
.codicon-stop-circle:before { content: '\eba5'; }
|
.codicon-stop-circle:before { content: '\eba5'; }
|
||||||
.codicon-play-circle:before { content: '\eba6'; }
|
.codicon-play-circle:before { content: '\eba6'; }
|
||||||
.codicon-record:before { content: '\eba7'; }
|
.codicon-record:before { content: '\eba7'; }
|
||||||
|
|
@ -466,7 +470,7 @@
|
||||||
.codicon-debug-rerun:before { content: '\ebc0'; }
|
.codicon-debug-rerun:before { content: '\ebc0'; }
|
||||||
.codicon-workspace-trusted:before { content: '\ebc1'; }
|
.codicon-workspace-trusted:before { content: '\ebc1'; }
|
||||||
.codicon-workspace-untrusted:before { content: '\ebc2'; }
|
.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-cmd:before { content: '\ebc4'; }
|
||||||
.codicon-terminal-debian:before { content: '\ebc5'; }
|
.codicon-terminal-debian:before { content: '\ebc5'; }
|
||||||
.codicon-terminal-linux:before { content: '\ebc6'; }
|
.codicon-terminal-linux:before { content: '\ebc6'; }
|
||||||
|
|
@ -500,6 +504,7 @@
|
||||||
.codicon-graph-line:before { content: '\ebe2'; }
|
.codicon-graph-line:before { content: '\ebe2'; }
|
||||||
.codicon-graph-scatter:before { content: '\ebe3'; }
|
.codicon-graph-scatter:before { content: '\ebe3'; }
|
||||||
.codicon-pie-chart:before { content: '\ebe4'; }
|
.codicon-pie-chart:before { content: '\ebe4'; }
|
||||||
|
.codicon-bracket:before { content: '\eb0f'; }
|
||||||
.codicon-bracket-dot:before { content: '\ebe5'; }
|
.codicon-bracket-dot:before { content: '\ebe5'; }
|
||||||
.codicon-bracket-error:before { content: '\ebe6'; }
|
.codicon-bracket-error:before { content: '\ebe6'; }
|
||||||
.codicon-lock-small:before { content: '\ebe7'; }
|
.codicon-lock-small:before { content: '\ebe7'; }
|
||||||
|
|
@ -519,20 +524,26 @@
|
||||||
.codicon-layout-statusbar:before { content: '\ebf5'; }
|
.codicon-layout-statusbar:before { content: '\ebf5'; }
|
||||||
.codicon-layout-menubar:before { content: '\ebf6'; }
|
.codicon-layout-menubar:before { content: '\ebf6'; }
|
||||||
.codicon-layout-centered:before { content: '\ebf7'; }
|
.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-target:before { content: '\ebf8'; }
|
||||||
.codicon-indent:before { content: '\ebf9'; }
|
.codicon-indent:before { content: '\ebf9'; }
|
||||||
.codicon-record-small:before { content: '\ebfa'; }
|
.codicon-record-small:before { content: '\ebfa'; }
|
||||||
.codicon-error-small:before { content: '\ebfb'; }
|
.codicon-error-small:before { content: '\ebfb'; }
|
||||||
|
.codicon-terminal-decoration-error:before { content: '\ebfb'; }
|
||||||
.codicon-arrow-circle-down:before { content: '\ebfc'; }
|
.codicon-arrow-circle-down:before { content: '\ebfc'; }
|
||||||
.codicon-arrow-circle-left:before { content: '\ebfd'; }
|
.codicon-arrow-circle-left:before { content: '\ebfd'; }
|
||||||
.codicon-arrow-circle-right:before { content: '\ebfe'; }
|
.codicon-arrow-circle-right:before { content: '\ebfe'; }
|
||||||
.codicon-arrow-circle-up:before { content: '\ebff'; }
|
.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-heart-filled:before { content: '\ec04'; }
|
||||||
.codicon-map:before { content: '\ec05'; }
|
.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-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-circle-small:before { content: '\ec07'; }
|
||||||
.codicon-bell-slash:before { content: '\ec08'; }
|
.codicon-bell-slash:before { content: '\ec08'; }
|
||||||
.codicon-bell-slash-dot:before { content: '\ec09'; }
|
.codicon-bell-slash-dot:before { content: '\ec09'; }
|
||||||
|
|
@ -544,3 +555,42 @@
|
||||||
.codicon-send:before { content: '\ec0f'; }
|
.codicon-send:before { content: '\ec0f'; }
|
||||||
.codicon-sparkle:before { content: '\ec10'; }
|
.codicon-sparkle:before { content: '\ec10'; }
|
||||||
.codicon-insert:before { content: '\ec11'; }
|
.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]);
|
}, [fetched, setFetched, setData]);
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<div data-testId='name'>{data.name}</div>
|
<div data-testid='name'>{data.name}</div>
|
||||||
<button onClick={() => {
|
<button onClick={() => {
|
||||||
setFetched(false);
|
setFetched(false);
|
||||||
setData({ name: '<none>' });
|
setData({ name: '<none>' });
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import os from 'os';
|
||||||
import { browserTest as it, expect } from '../config/browserTest';
|
import { browserTest as it, expect } from '../config/browserTest';
|
||||||
|
|
||||||
it.describe('mobile viewport', () => {
|
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 iPhone = playwright.devices['iPhone 6'];
|
||||||
const context = await browser.newContext({ ...iPhone });
|
const context = await browser.newContext({ ...iPhone });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
@ -63,7 +65,8 @@ it.describe('mobile viewport', () => {
|
||||||
await context.close();
|
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 context = await browser.newContext({ viewport: { width: 800, height: 600 }, hasTouch: true });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
await page.goto(server.EMPTY_PAGE);
|
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);
|
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');
|
await page.goto(server.PREFIX + '/mobile.html');
|
||||||
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false);
|
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false);
|
||||||
await page.goto(server.PREFIX + '/detect-touch.html');
|
await page.goto(server.PREFIX + '/detect-touch.html');
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import tls from 'tls';
|
import tls from 'tls';
|
||||||
|
import type https from 'https';
|
||||||
|
import zlib from 'zlib';
|
||||||
import type http2 from 'http2';
|
import type http2 from 'http2';
|
||||||
import type http from 'http';
|
import type http from 'http';
|
||||||
import { expect, playwrightTest as base } from '../config/browserTest';
|
import { expect, playwrightTest as base } from '../config/browserTest';
|
||||||
|
|
@ -303,6 +305,21 @@ test.describe('browser', () => {
|
||||||
await page.close();
|
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 }) => {
|
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) {
|
for (const tlsVersion of ['TLSv1.3', 'TLSv1.2'] as const) {
|
||||||
await test.step(`TLS version: ${tlsVersion}`, async () => {
|
await test.step(`TLS version: ${tlsVersion}`, async () => {
|
||||||
|
|
@ -360,34 +377,175 @@ test.describe('browser', () => {
|
||||||
await page.close();
|
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 serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
const page = await browser.newPage({
|
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,
|
ignoreHTTPSErrors: true,
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: new URL(serverURL).origin,
|
origin: new URL(serverURL).origin,
|
||||||
pfxPath: asset('client-certificates/client/trusted/cert-legacy.pfx'),
|
pfxPath: asset('client-certificates/client/trusted/cert-legacy.pfx'),
|
||||||
passphrase: 'secure'
|
passphrase: 'secure'
|
||||||
}],
|
}],
|
||||||
});
|
})).rejects.toThrow('Unsupported TLS certificate');
|
||||||
await page.goto(serverURL);
|
|
||||||
await expect(page.getByText('Unsupported TLS certificate.')).toBeVisible();
|
|
||||||
await page.close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw a http error if the pfx passphrase is incorect', async ({ browser, startCCServer, asset, browserName }) => {
|
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 serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
const page = await browser.newPage({
|
await expect(browser.newPage({
|
||||||
ignoreHTTPSErrors: true,
|
ignoreHTTPSErrors: true,
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: new URL(serverURL).origin,
|
origin: new URL(serverURL).origin,
|
||||||
pfxPath: asset('client-certificates/client/trusted/cert.pfx'),
|
pfxPath: asset('client-certificates/client/trusted/cert.pfx'),
|
||||||
passphrase: 'this-password-is-incorrect'
|
passphrase: 'this-password-is-incorrect'
|
||||||
}],
|
}],
|
||||||
});
|
})).rejects.toThrow('Failed to load client certificate: mac verify failure');
|
||||||
await page.goto(serverURL);
|
|
||||||
await expect(page.getByText('Playwright client-certificate error: mac verify failure')).toBeVisible();
|
|
||||||
await page.close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should pass with matching certificates on context APIRequestContext instance', async ({ browser, startCCServer, asset, browserName }) => {
|
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('safari-14-1', async ({ browser, browserName, platform, server, headless, isMac }) => {
|
||||||
it.skip(browserName !== 'webkit');
|
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({
|
const context = await browser.newContext({
|
||||||
deviceScaleFactor: 2
|
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('mobile-safari-14-1', async ({ playwright, browser, browserName, platform, isMac, server, headless }) => {
|
||||||
it.skip(browserName !== 'webkit');
|
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 iPhone = playwright.devices['iPhone 12'];
|
||||||
const context = await browser.newContext(iPhone);
|
const context = await browser.newContext(iPhone);
|
||||||
const { actual, expected } = await checkFeatures('mobile-safari-14-1', context, server);
|
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' });
|
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[] {
|
function toArray(x: any): any[] {
|
||||||
return Array.isArray(x) ? x : [x];
|
return Array.isArray(x) ? x : [x];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1439,3 +1439,15 @@ test('should show baseURL in metadata pane', {
|
||||||
await traceViewer.showMetadataTab();
|
await traceViewer.showMetadataTab();
|
||||||
await expect(traceViewer.metadataTab).toContainText('baseURL:https://example.com');
|
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);
|
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 }) => {
|
it('should work with content @smoke', async ({ page, server }) => {
|
||||||
await page.addInitScript({ content: 'window["injected"] = 123' });
|
await page.addInitScript({ content: 'window["injected"] = 123' });
|
||||||
await page.goto(server.PREFIX + '/tamperable.html');
|
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', {
|
it('should properly cancel Cross-Origin-Opener-Policy navigation', {
|
||||||
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32107' },
|
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) => {
|
server.setRoute('/empty.html', (req, res) => {
|
||||||
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
||||||
res.end();
|
res.end();
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,7 @@ it.describe('page screenshot', () => {
|
||||||
it('should work for webgl', async ({ page, server, browserName, platform }) => {
|
it('should work for webgl', async ({ page, server, browserName, platform }) => {
|
||||||
it.fixme(browserName === 'firefox');
|
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.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.setViewportSize({ width: 640, height: 480 });
|
||||||
await page.goto(server.PREFIX + '/screenshots/webgl.html');
|
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');
|
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 }) => {
|
test('exits successfully if there are no changes', async ({ runInlineTest, git, writeFiles }) => {
|
||||||
await writeFiles({
|
await writeFiles({
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
|
|
@ -429,3 +452,4 @@ test('exits successfully if there are no changes', async ({ runInlineTest, git,
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
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
|
trap "cd $(pwd -P)" EXIT
|
||||||
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
|
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")"
|
cd "$(dirname "$0")"
|
||||||
PACKAGE_VERSION=$(node -p "require('../../package.json').version")
|
PACKAGE_VERSION=$(node -p "require('../../package.json').version")
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,8 @@ class Member {
|
||||||
this.clazz = null;
|
this.clazz = null;
|
||||||
/** @type {Member=} */
|
/** @type {Member=} */
|
||||||
this.enclosingMethod = undefined;
|
this.enclosingMethod = undefined;
|
||||||
|
/** @type {Member=} */
|
||||||
|
this.parent = undefined;
|
||||||
this.async = false;
|
this.async = false;
|
||||||
this.alias = name;
|
this.alias = name;
|
||||||
this.overloadIndex = 0;
|
this.overloadIndex = 0;
|
||||||
|
|
@ -372,10 +374,11 @@ class Member {
|
||||||
this.args = new Map();
|
this.args = new Map();
|
||||||
if (this.kind === 'method')
|
if (this.kind === 'method')
|
||||||
this.enclosingMethod = this;
|
this.enclosingMethod = this;
|
||||||
const indexType = type => {
|
const indexArg = (/** @type {Member} */ arg) => {
|
||||||
type.deepProperties().forEach(p => {
|
arg.type?.deepProperties().forEach(p => {
|
||||||
p.enclosingMethod = this;
|
p.enclosingMethod = this;
|
||||||
indexType(p.type);
|
p.parent = arg;
|
||||||
|
indexArg(p);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const arg of this.argsArray) {
|
for (const arg of this.argsArray) {
|
||||||
|
|
@ -385,7 +388,7 @@ class Member {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
arg.type.properties.sort((p1, p2) => p1.name.localeCompare(p2.name));
|
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) {
|
function serializeProperty(arg) {
|
||||||
const result = { ...arg };
|
const result = { ...arg, parent: undefined };
|
||||||
sanitize(result);
|
sanitize(result);
|
||||||
if (arg.type)
|
if (arg.type)
|
||||||
result.type = serializeType(arg.type, arg.name === 'options');
|
result.type = serializeType(arg.type, arg.name === 'options');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue