diff --git a/.eslintrc.js b/.eslintrc.js index bb351a8c56..a116a37036 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,9 +6,14 @@ module.exports = { sourceType: "module", }, extends: [ + "plugin:react/recommended", "plugin:react-hooks/recommended" ], + settings: { + react: { version: "18" } + }, + /** * ESLint rules * @@ -124,5 +129,8 @@ module.exports = { "mustMatch": "Copyright", "templateFile": require("path").join(__dirname, "utils", "copyright.js"), }], + + // react + "react/react-in-jsx-scope": 0 } }; diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 6d5558ae55..7b6c6aab1d 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -415,13 +415,42 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte [`method: Page.addInitScript`] is not defined. ::: +**Bundling** + +If you have a complex script split into several files, it needs to be bundled into a single file first. We recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a commonjs module and pass [`option: path`] and [`option: arg`]. + +```js browser title="mocks/mockRandom.ts" +// This script can import other files. +import { defaultValue } from './defaultValue'; + +export default function(value?: number) { + window.Math.random = () => value ?? defaultValue; +} +``` + +```sh +# bundle with esbuild +esbuild mocks/mockRandom.ts --bundle --format=cjs --outfile=mocks/mockRandom.js +``` + +```js title="tests/example.spec.ts" +const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') }; + +// Passing 42 as an argument to the default export function. +await context.addInitScript({ path: mockPath }, 42); + +// Make sure to pass undefined even if you do not need to pass an argument. +// This instructs Playwright to treat the file as a commonjs module. +await context.addInitScript({ path: mockPath }, undefined); +``` + ### param: BrowserContext.addInitScript.script * since: v1.8 * langs: js - `script` <[function]|[string]|[Object]> - `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the - current working directory. Optional. - - `content` ?<[string]> Raw script content. Optional. + current working directory. + - `content` ?<[string]> Raw script content. Script to be evaluated in all pages in the browser context. @@ -437,7 +466,9 @@ Script to be evaluated in all pages in the browser context. * langs: js - `arg` ?<[Serializable]> -Optional argument to pass to [`param: script`] (only supported when passing a function). +Optional JSON-serializable argument to pass to [`param: script`]. +* When `script` is a function, the argument is passed to it directly. +* When `script` is a file path, the file is assumed to be a commonjs module. The default export, either `module.exports` or `module.exports.default`, should be a function that's going to be executed with this argument. ### param: BrowserContext.addInitScript.path * since: v1.8 diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index ee9623c867..11d7cbdd74 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -619,13 +619,42 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte [`method: Page.addInitScript`] is not defined. ::: +**Bundling** + +If you have a complex script split into several files, it needs to be bundled into a single file first. We recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a commonjs module and pass [`option: path`] and [`option: arg`]. + +```js browser title="mocks/mockRandom.ts" +// This script can import other files. +import { defaultValue } from './defaultValue'; + +export default function(value?: number) { + window.Math.random = () => value ?? defaultValue; +} +``` + +```sh +# bundle with esbuild +esbuild mocks/mockRandom.ts --bundle --format=cjs --outfile=mocks/mockRandom.js +``` + +```js title="tests/example.spec.ts" +const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') }; + +// Passing 42 as an argument to the default export function. +await page.addInitScript({ path: mockPath }, 42); + +// Make sure to pass undefined even if you do not need to pass an argument. +// This instructs Playwright to treat the file as a commonjs module. +await page.addInitScript({ path: mockPath }, undefined); +``` + ### param: Page.addInitScript.script * since: v1.8 * langs: js - `script` <[function]|[string]|[Object]> - `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the - current working directory. Optional. - - `content` ?<[string]> Raw script content. Optional. + current working directory. + - `content` ?<[string]> Raw script content. Script to be evaluated in the page. @@ -641,7 +670,9 @@ Script to be evaluated in all pages in the browser context. * langs: js - `arg` ?<[Serializable]> -Optional argument to pass to [`param: script`] (only supported when passing a function). +Optional JSON-serializable argument to pass to [`param: script`]. +* When `script` is a function, the argument is passed to it directly. +* When `script` is a file path, the file is assumed to be a commonjs module. The default export, either `module.exports` or `module.exports.default`, should be a function that's going to be executed with this argument. ### param: Page.addInitScript.path * since: v1.8 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index f4adc23a3c..5aa9c6a5ab 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -531,15 +531,18 @@ Does not enforce fixed viewport, allows resizing window in the headed mode. - `clientCertificates` <[Array]<[Object]>> - `origin` <[string]> Exact origin that the certificate is valid for. Origin includes `https` protocol, a hostname and optionally a port. - `certPath` ?<[path]> Path to the file with the certificate in PEM format. + - `cert` ?<[Buffer]> Direct value of the certificate in PEM format. - `keyPath` ?<[path]> Path to the file with the private key in PEM format. + - `key` ?<[Buffer]> Direct value of the private key in PEM format. - `pfxPath` ?<[path]> Path to the PFX or PKCS12 encoded private key and certificate chain. + - `pfx` ?<[Buffer]> Direct value of the PFX or PKCS12 encoded private key and certificate chain. - `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX). TLS Client Authentication allows the server to request a client certificate and verify it. **Details** -An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. +An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`, a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally, `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. :::note Using Client Certificates in combination with Proxy Servers is not supported. diff --git a/docs/src/best-practices-js.md b/docs/src/best-practices-js.md index 7a57193699..3b70817923 100644 --- a/docs/src/best-practices-js.md +++ b/docs/src/best-practices-js.md @@ -261,11 +261,11 @@ npx playwright --version Setup CI/CD and run your tests frequently. The more often you run your tests the better. Ideally you should run your tests on each commit and pull request. Playwright comes with a [GitHub actions workflow](/ci-intro.md) so that tests will run on CI for you with no setup required. Playwright can also be setup on the [CI environment](/ci.md) of your choice. -Use Linux when running your tests on CI as it is cheaper. Developers can use whatever environment when running locally but use linux on CI. +Use Linux when running your tests on CI as it is cheaper. Developers can use whatever environment when running locally but use linux on CI. Consider setting up [Sharding](./test-sharding.md) to make CI faster. ### Lint your tests -Linting the tests helps catching errors early. Use [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises/) [ESLint](https://eslint.org) rule to make sure there are no missing awaits before the asynchronous calls to the Playwright API. +We recommend TypeScript and linting with ESLint for your tests to catch errors early. Use [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises/) [ESLint](https://eslint.org) rule to make sure there are no missing awaits before the asynchronous calls to the Playwright API. On your CI you can run `tsc --noEmit` to ensure that functions are called with the right signature. ### Use parallelism and sharding diff --git a/docs/src/browsers.md b/docs/src/browsers.md index c1ad722d60..3b9e76d006 100644 --- a/docs/src/browsers.md +++ b/docs/src/browsers.md @@ -461,7 +461,7 @@ Playwright's Firefox version matches the recent [Firefox Stable](https://www.moz ### WebKit -Playwright's WebKit is derived from the latest WebKit main branch sources, often before these updates are incorporated into Apple Safari and other WebKit-based browsers. This gives a lot of lead time to react on the potential browser update issues. Playwright doesn't work with the branded version of Safari since it relies on patches. Instead, you can test using the most recent WebKit build. Note that avialability of certain features, which depend heavily on the underlying platform, may vary between operating systems. +Playwright's WebKit is derived from the latest WebKit main branch sources, often before these updates are incorporated into Apple Safari and other WebKit-based browsers. This gives a lot of lead time to react on the potential browser update issues. Playwright doesn't work with the branded version of Safari since it relies on patches. Instead, you can test using the most recent WebKit build. Note that availability of certain features, which depend heavily on the underlying platform, may vary between operating systems. ## Install behind a firewall or a proxy diff --git a/docs/src/ci-intro.md b/docs/src/ci-intro.md index bad1e358c2..492130088a 100644 --- a/docs/src/ci-intro.md +++ b/docs/src/ci-intro.md @@ -1,19 +1,17 @@ --- id: ci-intro -title: "CI GitHub Actions" +title: "Setting up CI" --- ## Introduction * langs: js -Playwright tests can be run on any CI provider. In this section we will cover running tests on GitHub using GitHub actions. If you would like to see how to configure other CI providers check out our detailed [doc on Continuous Integration](./ci.md). - -When [installing Playwright](./intro.md) using the [VS Code extension](./getting-started-vscode.md) or with `npm init playwright@latest` you are given the option to add a [GitHub Actions](https://docs.github.com/en/actions) workflow. This creates a `playwright.yml` file inside a `.github/workflows` folder containing everything you need so that your tests run on each push and pull request into the main/master branch. +Playwright tests can be run on any CI provider. This guide covers one way of running tests on GitHub using GitHub actions. If you would like to learn more, or how to configure other CI providers, check out our detailed [doc on Continuous Integration](./ci.md). #### You will learn * langs: js -- [How to run tests on push/pull_request](/ci-intro.md#on-pushpull_request) +- [How to set up GitHub Actions](/ci-intro.md#setting-up-github-actions) - [How to view test logs](/ci-intro.md#viewing-test-logs) - [How to view the HTML report](/ci-intro.md#viewing-the-html-report) - [How to view the trace](/ci-intro.md#viewing-the-trace) @@ -25,22 +23,18 @@ When [installing Playwright](./intro.md) using the [VS Code extension](./getting Playwright tests can be ran on any CI provider. In this section we will cover running tests on GitHub using GitHub actions. If you would like to see how to configure other CI providers check out our detailed doc on Continuous Integration. -To add a [GitHub Actions](https://docs.github.com/en/actions) file first create `.github/workflows` folder and inside it add a `playwright.yml` file containing the example code below so that your tests will run on each push and pull request for the main/master branch. - #### You will learn * langs: python, java, csharp -- [How to run tests on push/pull_request](/ci-intro.md#on-pushpull_request) +- [How to set up GitHub Actions](/ci-intro.md#setting-up-github-actions) - [How to view test logs](/ci-intro.md#viewing-test-logs) - [How to view the trace](/ci-intro.md#viewing-the-trace) ## Setting up GitHub Actions - -### On push/pull_request * langs: js -Tests will run on push or pull request on branches main/master. The [workflow](https://docs.github.com/en/actions/using-workflows/about-workflows) will install all dependencies, install Playwright and then run the tests. It will also create the HTML report. +When [installing Playwright](./intro.md) using the [VS Code extension](./getting-started-vscode.md) or with `npm init playwright@latest` you are given the option to add a [GitHub Actions](https://docs.github.com/en/actions) workflow. This creates a `playwright.yml` file inside a `.github/workflows` folder containing everything you need so that your tests run on each push and pull request into the main/master branch. Here's how that file looks: ```yml js title=".github/workflows/playwright.yml" name: Playwright Tests @@ -57,7 +51,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: lts/* - name: Install dependencies run: npm ci - name: Install Playwright Browsers @@ -72,10 +66,21 @@ jobs: retention-days: 30 ``` -### On push/pull_request +The workflow performs these steps: + +1. Clone your repository +2. Install Node.js +3. Install NPM Dependencies +4. Install Playwright Browsers +5. Run Playwright tests +6. Upload HTML report to the GitHub UI + +To learn more about this, see ["Understanding GitHub Actions"](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions). + +## Setting up GitHub Actions * langs: python, java, csharp -Tests will run on push or pull request on branches main/master. The [workflow](https://docs.github.com/en/actions/using-workflows/about-workflows) will install all dependencies, install Playwright and then run the tests. +To add a [GitHub Actions](https://docs.github.com/en/actions) file first create `.github/workflows` folder and inside it add a `playwright.yml` file containing the example code below so that your tests will run on each push and pull request for the main/master branch. ```yml python title=".github/workflows/playwright.yml" name: Playwright Tests @@ -128,7 +133,7 @@ jobs: java-version: '17' - name: Build & Install run: mvn -B install -D skipTests --no-transfer-progress - - name: Install Playwright + - name: Ensure browsers are installed run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps" - name: Run tests run: mvn test @@ -151,275 +156,23 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - - run: dotnet build + - name: Build & Install + run: dotnet build - name: Ensure browsers are installed run: pwsh bin/Debug/net8.0/playwright.ps1 install --with-deps - name: Run your tests run: dotnet test ``` -### On push/pull_request (sharded) -* langs: js +To learn more about this, see ["Understanding GitHub Actions"](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions). -GitHub Actions supports [sharding tests between multiple jobs](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs). Check out our [sharding doc](./test-sharding) to learn more about sharding and to see a [GitHub actions example](./test-sharding.md#github-actions-example) of how to configure a job to run your tests on multiple machines as well as how to merge the HTML reports. +Looking at the list of steps in `jobs.test.steps`, you can see that the workflow performs these steps: -### Via Containers - -GitHub Actions support [running jobs in a container](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container) by using the [`jobs..container`](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainer) option. This is useful to not pollute the host environment with dependencies and to have a consistent environment for e.g. screenshots/visual regression testing across different operating systems. - -```yml js title=".github/workflows/playwright.yml" -name: Playwright Tests -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] -jobs: - playwright: - name: 'Playwright Tests' - runs-on: ubuntu-latest - container: - image: mcr.microsoft.com/playwright:v%%VERSION%%-jammy - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 18 - - name: Install dependencies - run: npm ci - - name: Run your tests - run: npx playwright test - env: - HOME: /root -``` - -```yml python title=".github/workflows/playwright.yml" -name: Playwright Tests -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] -jobs: - playwright: - name: 'Playwright Tests' - runs-on: ubuntu-latest - container: - image: mcr.microsoft.com/playwright/python:v%%VERSION%%-jammy - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r local-requirements.txt - pip install -e . - - name: Run your tests - run: pytest - env: - HOME: /root -``` - -```yml java title=".github/workflows/playwright.yml" -name: Playwright Tests -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] -jobs: - playwright: - name: 'Playwright Tests' - runs-on: ubuntu-latest - container: - image: mcr.microsoft.com/playwright/java:v%%VERSION%%-jammy - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' - - name: Build & Install - run: mvn -B install -D skipTests --no-transfer-progress - - name: Run tests - run: mvn test - env: - HOME: /root -``` - -```yml csharp title=".github/workflows/playwright.yml" -name: Playwright Tests -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] -jobs: - playwright: - name: 'Playwright Tests' - runs-on: ubuntu-latest - container: - image: mcr.microsoft.com/playwright/dotnet:v%%VERSION%%-jammy - steps: - - uses: actions/checkout@v4 - - name: Setup dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - run: dotnet build - - name: Run your tests - run: dotnet test - env: - HOME: /root -``` - -### On deployment - -This will start the tests after a [GitHub Deployment](https://developer.github.com/v3/repos/deployments/) went into the `success` state. -Services like Vercel use this pattern so you can run your end-to-end tests on their deployed environment. - -```yml js title=".github/workflows/playwright.yml" -name: Playwright Tests -on: - deployment_status: -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - if: github.event.deployment_status.state == 'success' - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 18 - - name: Install dependencies - run: npm ci - - name: Install Playwright - run: npx playwright install --with-deps - - name: Run Playwright tests - run: npx playwright test - env: - PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }} -``` - -```yml python title=".github/workflows/playwright.yml" -name: Playwright Tests -on: - deployment_status: -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - if: github.event.deployment_status.state == 'success' - steps: - - uses: actions/checkout@v4 - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - name: Ensure browsers are installed - run: python -m playwright install --with-deps - - name: Run tests - run: pytest - env: - # This might depend on your test-runner - PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }} -``` - -```yml java title=".github/workflows/playwright.yml" -name: Playwright Tests -on: - deployment_status: -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - if: github.event.deployment_status.state == 'success' - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' - - name: Build & Install - run: mvn -B install -D skipTests --no-transfer-progress - - name: Install Playwright - run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps" - - name: Run tests - run: mvn test - env: - # This might depend on your test-runner - PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }} -``` - -```yml csharp title=".github/workflows/playwright.yml" -name: Playwright Tests -on: - deployment_status: -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - if: github.event.deployment_status.state == 'success' - steps: - - uses: actions/checkout@v4 - - name: Setup dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - run: dotnet build - - name: Ensure browsers are installed - run: pwsh bin/Debug/net8.0/playwright.ps1 install --with-deps - - name: Run tests - run: dotnet test - env: - # This might depend on your test-runner - PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }} -``` - -### Fail-Fast -* langs: js - -Even with sharding enabled, large test suites can take very long to execute. Running changed test files first on PRs will give you a faster feedback loop and use less CI resources. - -```yml js title=".github/workflows/playwright.yml" -name: Playwright Tests -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 18 - - name: Install dependencies - run: npm ci - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - name: Run changed Playwright tests - run: npx playwright test --only-changed=$GITHUB_BASE_REF - if: github.event_name == 'pull_request' - - name: Run Playwright tests - run: npx playwright test - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 -``` +1. Clone your repository +2. Install language dependencies +3. Install project dependencies and build +4. Install Playwright Browsers +5. Run tests ## Create a Repo and Push to GitHub @@ -569,4 +322,5 @@ This step will not work for pull requests created from a forked repository becau - [Learn how to perform Actions](./input.md) - [Learn how to write Assertions](./test-assertions.md) - [Learn more about the Trace Viewer](/trace-viewer.md) -- [Learn more about running tests on other CI providers](/ci.md) +- [Learn more ways of running tests on GitHub Actions](/ci.md) +- [Learn more about running tests on other CI providers](/ci.md#github-actions) // TODO: is this link correct? \ No newline at end of file diff --git a/docs/src/ci.md b/docs/src/ci.md index 624ccfa447..7a08a00b4a 100644 --- a/docs/src/ci.md +++ b/docs/src/ci.md @@ -59,14 +59,398 @@ export default defineConfig({ }); ``` - ## CI configurations -The [Command line tools](./browsers#install-system-dependencies) can be used to install all operating system dependencies on GitHub Actions. +The [Command line tools](./browsers#install-system-dependencies) can be used to install all operating system dependencies in CI. ### GitHub Actions -Check out our [GitHub Actions](ci-intro.md) guide for more information on how to run your tests on GitHub. +#### On push/pull_request +* langs: js + +Tests will run on push or pull request on branches main/master. The [workflow](https://docs.github.com/en/actions/using-workflows/about-workflows) will install all dependencies, install Playwright and then run the tests. It will also create the HTML report. + +```yml js title=".github/workflows/playwright.yml" +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 +``` + +#### On push/pull_request +* langs: python, java, csharp + +Tests will run on push or pull request on branches main/master. The [workflow](https://docs.github.com/en/actions/using-workflows/about-workflows) will install all dependencies, install Playwright and then run the tests. + +```yml python title=".github/workflows/playwright.yml" +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Ensure browsers are installed + run: python -m playwright install --with-deps + - name: Run your tests + run: pytest --tracing=retain-on-failure + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-traces + path: test-results/ +``` + +```yml java title=".github/workflows/playwright.yml" +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + - name: Build & Install + run: mvn -B install -D skipTests --no-transfer-progress + - name: Ensure browsers are installed + run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps" + - name: Run tests + run: mvn test +``` + +```yml csharp title=".github/workflows/playwright.yml" +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - name: Build & Install + run: dotnet build + - name: Ensure browsers are installed + run: pwsh bin/Debug/net8.0/playwright.ps1 install --with-deps + - name: Run your tests + run: dotnet test +``` + +#### On push/pull_request (sharded) +* langs: js + +GitHub Actions supports [sharding tests between multiple jobs](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs). Check out our [sharding doc](./test-sharding) to learn more about sharding and to see a [GitHub actions example](./test-sharding.md#github-actions-example) of how to configure a job to run your tests on multiple machines as well as how to merge the HTML reports. + +#### Via Containers + +GitHub Actions support [running jobs in a container](https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container) by using the [`jobs..container`](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainer) option. This is useful to not pollute the host environment with dependencies and to have a consistent environment for e.g. screenshots/visual regression testing across different operating systems. + +```yml js title=".github/workflows/playwright.yml" +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + playwright: + name: 'Playwright Tests' + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v%%VERSION%%-jammy + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + - name: Run your tests + run: npx playwright test + env: + HOME: /root +``` + +```yml python title=".github/workflows/playwright.yml" +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + playwright: + name: 'Playwright Tests' + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright/python:v%%VERSION%%-jammy + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r local-requirements.txt + pip install -e . + - name: Run your tests + run: pytest + env: + HOME: /root +``` + +```yml java title=".github/workflows/playwright.yml" +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + playwright: + name: 'Playwright Tests' + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright/java:v%%VERSION%%-jammy + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + - name: Build & Install + run: mvn -B install -D skipTests --no-transfer-progress + - name: Run tests + run: mvn test + env: + HOME: /root +``` + +```yml csharp title=".github/workflows/playwright.yml" +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + playwright: + name: 'Playwright Tests' + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright/dotnet:v%%VERSION%%-jammy + steps: + - uses: actions/checkout@v4 + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - run: dotnet build + - name: Run your tests + run: dotnet test + env: + HOME: /root +``` + +#### On deployment + +This will start the tests after a [GitHub Deployment](https://developer.github.com/v3/repos/deployments/) went into the `success` state. +Services like Vercel use this pattern so you can run your end-to-end tests on their deployed environment. + +```yml js title=".github/workflows/playwright.yml" +name: Playwright Tests +on: + deployment_status: +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + if: github.event.deployment_status.state == 'success' + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + - name: Install Playwright + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + env: + PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }} +``` + +```yml python title=".github/workflows/playwright.yml" +name: Playwright Tests +on: + deployment_status: +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + if: github.event.deployment_status.state == 'success' + steps: + - uses: actions/checkout@v4 + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Ensure browsers are installed + run: python -m playwright install --with-deps + - name: Run tests + run: pytest + env: + # This might depend on your test-runner + PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }} +``` + +```yml java title=".github/workflows/playwright.yml" +name: Playwright Tests +on: + deployment_status: +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + if: github.event.deployment_status.state == 'success' + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + - name: Build & Install + run: mvn -B install -D skipTests --no-transfer-progress + - name: Install Playwright + run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps" + - name: Run tests + run: mvn test + env: + # This might depend on your test-runner + PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }} +``` + +```yml csharp title=".github/workflows/playwright.yml" +name: Playwright Tests +on: + deployment_status: +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + if: github.event.deployment_status.state == 'success' + steps: + - uses: actions/checkout@v4 + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - run: dotnet build + - name: Ensure browsers are installed + run: pwsh bin/Debug/net8.0/playwright.ps1 install --with-deps + - name: Run tests + run: dotnet test + env: + # This might depend on your test-runner + PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }} +``` + +#### Fail-Fast +* langs: js + +Large test suites can take very long to execute. By executing a preliminary test run with the `--only-changed` flag, you can run test files that are likely to fail first. +This will give you a faster feedback loop and slightly lower CI consumption while working on Pull Requests. +To detect test files affected by your changeset, `--only-changed` analyses your suites' dependency graph. This is a heuristic and might miss tests, so it's important that you always run the full test suite after the preliminary test run. + +```yml js title=".github/workflows/playwright.yml" {20-23} +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run changed Playwright tests + run: npx playwright test --only-changed=$GITHUB_BASE_REF + if: github.event_name == 'pull_request' + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 +``` ### Docker @@ -660,4 +1044,4 @@ xvfb-run mvn test ``` ```bash csharp xvfb-run dotnet test -``` +``` \ No newline at end of file diff --git a/docs/src/evaluating.md b/docs/src/evaluating.md index 50851110d0..903aeb8a90 100644 --- a/docs/src/evaluating.md +++ b/docs/src/evaluating.md @@ -68,9 +68,95 @@ int status = await page.EvaluateAsync(@"async () => { }"); ``` +## Different environments + +Evaluated scripts run in the browser environment, while your test runs in a testing environments. This means you cannot use variables from your test in the page and vice versa. Instead, you should pass them explicitly as an argument. + +The following snippet is **WRONG** because it uses the variable directly: + +```js +const data = 'some data'; +const result = await page.evaluate(() => { + // WRONG: there is no "data" in the web page. + window.myApp.use(data); +}); +``` + +```java +String data = "some data"; +Object result = page.evaluate("() => {\n" + + " // WRONG: there is no 'data' in the web page.\n" + + " window.myApp.use(data);\n" + + "}"); +``` + +```python async +data = "some data" +result = await page.evaluate("""() => { + // WRONG: there is no "data" in the web page. + window.myApp.use(data) +}""") +``` + +```python sync +data = "some data" +result = page.evaluate("""() => { + // WRONG: there is no "data" in the web page. + window.myApp.use(data) +}""") +``` + +```csharp +var data = "some data"; +var result = await page.EvaluateAsync(@"() => { + // WRONG: there is no 'data' in the web page. + window.myApp.use(data); +}"); +``` + +The following snippet is **CORRECT** because it passes the value explicitly as an argument: + +```js +const data = 'some data'; +// Pass |data| as a parameter. +const result = await page.evaluate(data => { + window.myApp.use(data); +}, data); +``` + +```java +String data = "some data"; +// Pass |data| as a parameter. +Object result = page.evaluate("data => {\n" + + " window.myApp.use(data);\n" + + "}", data); +``` + +```python async +data = "some data" +# Pass |data| as a parameter. +result = await page.evaluate("""data => { + window.myApp.use(data) +}""", data) +``` + +```python sync +data = "some data" +# Pass |data| as a parameter. +result = page.evaluate("""data => { + window.myApp.use(data) +}""", data) +``` + +```csharp +var data = "some data"; +// Pass |data| as a parameter. +var result = await page.EvaluateAsync("data => { window.myApp.use(data); }", data); +``` + ## Evaluation Argument -Playwright evaluation methods like [`method: Page.evaluate`] take a single optional argument. This argument can be a mix of [Serializable] values and [JSHandle] or [ElementHandle] instances. Handles are automatically converted to the value they represent. +Playwright evaluation methods like [`method: Page.evaluate`] take a single optional argument. This argument can be a mix of [Serializable] values and [JSHandle] instances. Handles are automatically converted to the value they represent. ```js // A primitive value. @@ -86,7 +172,7 @@ await page.evaluate(object => object.foo, { foo: 'bar' }); const button = await page.evaluateHandle('window.button'); await page.evaluate(button => button.textContent, button); -// Alternative notation using elementHandle.evaluate. +// Alternative notation using JSHandle.evaluate. await button.evaluate((button, from) => button.textContent.substring(from), 5); // Object with multiple handles. @@ -109,7 +195,7 @@ await page.evaluate( ([b1, b2]) => b1.textContent + b2.textContent, [button1, button2]); -// Any non-cyclic mix of serializables and handles works. +// Any mix of serializables and handles works. await page.evaluate( x => x.button1.textContent + x.list[0].textContent + String(x.foo), { button1, list: [button2], foo: null }); @@ -131,7 +217,7 @@ page.evaluate("object => object.foo", obj); ElementHandle button = page.evaluateHandle("window.button"); page.evaluate("button => button.textContent", button); -// Alternative notation using elementHandle.evaluate. +// Alternative notation using JSHandle.evaluate. button.evaluate("(button, from) => button.textContent.substring(from)", 5); // Object with multiple handles. @@ -156,7 +242,7 @@ page.evaluate( "([b1, b2]) => b1.textContent + b2.textContent", Arrays.asList(button1, button2)); -// Any non-cyclic mix of serializables and handles works. +// Any mix of serializables and handles works. Map arg = new HashMap<>(); arg.put("button1", button1); arg.put("list", Arrays.asList(button2)); @@ -180,7 +266,7 @@ await page.evaluate('object => object.foo', { 'foo': 'bar' }) button = await page.evaluate_handle('button') await page.evaluate('button => button.textContent', button) -# Alternative notation using elementHandle.evaluate. +# Alternative notation using JSHandle.evaluate. await button.evaluate('(button, from) => button.textContent.substring(from)', 5) # Object with multiple handles. @@ -203,7 +289,7 @@ await page.evaluate(""" ([b1, b2]) => b1.textContent + b2.textContent""", [button1, button2]) -# Any non-cyclic mix of serializables and handles works. +# Any mix of serializables and handles works. await page.evaluate(""" x => x.button1.textContent + x.list[0].textContent + String(x.foo)""", { 'button1': button1, 'list': [button2], 'foo': None }) @@ -223,7 +309,7 @@ page.evaluate('object => object.foo', { 'foo': 'bar' }) button = page.evaluate_handle('window.button') page.evaluate('button => button.textContent', button) -# Alternative notation using elementHandle.evaluate. +# Alternative notation using JSHandle.evaluate. button.evaluate('(button, from) => button.textContent.substring(from)', 5) # Object with multiple handles. @@ -245,7 +331,7 @@ page.evaluate(""" ([b1, b2]) => b1.textContent + b2.textContent""", [button1, button2]) -# Any non-cyclic mix of serializables and handles works. +# Any mix of serializables and handles works. page.evaluate(""" x => x.button1.textContent + x.list[0].textContent + String(x.foo)""", { 'button1': button1, 'list': [button2], 'foo': None }) @@ -265,7 +351,7 @@ await page.EvaluateAsync("object => object.foo", new { foo = "bar" }); var button = await page.EvaluateHandleAsync("window.button"); await page.EvaluateAsync("button => button.textContent", button); -// Alternative notation using elementHandle.EvaluateAsync. +// Alternative notation using JSHandle.EvaluateAsync. await button.EvaluateAsync("(button, from) => button.textContent.substring(from)", 5); // Object with multiple handles. @@ -282,93 +368,69 @@ await page.EvaluateAsync("({ button1, button2 }) => button1.textContent + button // Note the required parenthesis. await page.EvaluateAsync("([b1, b2]) => b1.textContent + b2.textContent", new[] { button1, button2 }); -// Any non-cyclic mix of serializables and handles works. +// Any mix of serializables and handles works. await page.EvaluateAsync("x => x.button1.textContent + x.list[0].textContent + String(x.foo)", new { button1, list = new[] { button2 }, foo = null as object }); ``` -Right: +## Init scripts + +Sometimes it is convenient to evaluate something in the page before it starts loading. For example, you might want to setup some mocks or test data. + +In this case, use [`method: Page.addInitScript`] or [`method: BrowserContext.addInitScript`]. In the example below, we will replace `Math.random()` with a constant value. + +First, create a `preload.js` file that contains the mock. + +```js browser +// preload.js +Math.random = () => 42; +``` + +Next, add init script to the page. ```js -const data = { text: 'some data', value: 1 }; -// Pass |data| as a parameter. -const result = await page.evaluate(data => { - window.myApp.use(data); -}, data); -``` +import { test, expect } from '@playwright/test'; +import path from 'path'; -```java -Map data = new HashMap<>(); -data.put("text", "some data"); -data.put("value", 1); -// Pass |data| as a parameter. -Object result = page.evaluate("data => {\n" + - " window.myApp.use(data);\n" + - "}", data); -``` - -```python async -data = { 'text': 'some data', 'value': 1 } -# Pass |data| as a parameter. -result = await page.evaluate("""data => { - window.myApp.use(data) -}""", data) -``` - -```python sync -data = { 'text': 'some data', 'value': 1 } -# Pass |data| as a parameter. -result = page.evaluate("""data => { - window.myApp.use(data) -}""", data) -``` - -```csharp -var data = new { text = "some data", value = 1}; -// Pass data as a parameter -var result = await page.EvaluateAsync("data => { window.myApp.use(data); }", data); -``` - -Wrong: - -```js -const data = { text: 'some data', value: 1 }; -const result = await page.evaluate(() => { - // There is no |data| in the web page. - window.myApp.use(data); +test.beforeEach(async ({ page }) => { + // Add script for every test in the beforeEach hook. + // Make sure to correctly resolve the script path. + await page.addInitScript({ path: path.resolve(__dirname, '../mocks/preload.js') }); }); ``` ```java -Map data = new HashMap<>(); -data.put("text", "some data"); -data.put("value", 1); -Object result = page.evaluate("() => {\n" + - " // There is no |data| in the web page.\n" + - " window.myApp.use(data);\n" + - "}"); +// In your test, assuming the "preload.js" file is in the "mocks" directory. +page.addInitScript(Paths.get("mocks/preload.js")); ``` ```python async -data = { 'text': 'some data', 'value': 1 } -result = await page.evaluate("""() => { - // There is no |data| in the web page. - window.myApp.use(data) -}""") +# In your test, assuming the "preload.js" file is in the "mocks" directory. +await page.add_init_script(path="mocks/preload.js") ``` ```python sync -data = { 'text': 'some data', 'value': 1 } -result = page.evaluate("""() => { - // There is no |data| in the web page. - window.myApp.use(data) -}""") +# In your test, assuming the "preload.js" file is in the "mocks" directory. +page.add_init_script(path="mocks/preload.js") ``` ```csharp -var data = new { text = "some data", value = 1}; -// Pass data as a parameter -var result = await page.EvaluateAsync(@"data => { - // There is no |data| in the web page. - window.myApp.use(data); -}"); +// In your test, assuming the "preload.js" file is in the "mocks" directory. +await Page.AddInitScriptAsync(scriptPath: "mocks/preload.js"); +``` + +###### +* langs: js + +Alternatively, you can pass a function instead of creating a preload script file. This is more convenient for short or one-off scripts. You can also pass an argument this way. + +```js +import { test, expect } from '@playwright/test'; + +// Add script for every test in the beforeEach hook. +test.beforeEach(async ({ page }) => { + const value = 42; + await page.addInitScript(value => { + Math.random = () => value; + }, value); +}); ``` diff --git a/package-lock.json b/package-lock.json index c88a0381a3..1c48c213d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,8 +47,8 @@ "eslint": "^8.55.0", "eslint-plugin-internal-playwright": "file:utils/eslint-plugin-internal-playwright", "eslint-plugin-notice": "^0.9.10", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.3.0", + "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react-hooks": "^4.6.2", "formidable": "^2.1.1", "license-checker": "^25.0.1", "mime": "^3.0.0", @@ -2418,13 +2418,16 @@ } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2440,15 +2443,16 @@ } }, "node_modules/array-includes": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", - "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" }, "engines": { @@ -2467,6 +2471,26 @@ "node": ">=8" } }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", @@ -2504,30 +2528,34 @@ } }, "node_modules/array.prototype.tosorted": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", - "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -2543,20 +2571,14 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, - "node_modules/asynciterator.prototype": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", - "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "dependencies": { - "has-symbols": "^1.0.3" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true, + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2720,14 +2742,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3039,6 +3066,57 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -3132,17 +3210,20 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { @@ -3280,50 +3361,57 @@ } }, "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -3332,37 +3420,73 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-iterator-helpers": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", - "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "dev": true, "dependencies": { - "asynciterator.prototype": "^1.0.0", - "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.1", - "es-set-tostringtag": "^2.0.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", + "internal-slot": "^1.0.7", "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.0.1" + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -3528,39 +3652,41 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.33.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", - "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "version": "7.35.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", + "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", "dev": true, "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.12", + "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", + "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.8" + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, "engines": { "node": ">=10" @@ -4096,16 +4222,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4126,13 +4256,14 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -4368,21 +4499,21 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "dev": true, "engines": { "node": ">= 0.4" @@ -4404,12 +4535,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -4419,9 +4550,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "dependencies": { "function-bind": "^1.1.2" @@ -4524,12 +4655,12 @@ "dev": true }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", + "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" }, @@ -4538,14 +4669,16 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4630,6 +4763,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -4703,18 +4851,21 @@ } }, "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "engines": { "node": ">= 0.4" @@ -4781,21 +4932,27 @@ } }, "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4832,12 +4989,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -4847,10 +5004,13 @@ } }, "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4868,13 +5028,16 @@ } }, "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4891,6 +5054,12 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5467,28 +5636,29 @@ } }, "node_modules/object.entries": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", - "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/object.fromentries": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", - "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -5497,28 +5667,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.hasown": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", - "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.values": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", - "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -5736,6 +5893,15 @@ "resolved": "packages/playwright-webkit", "link": true }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", @@ -5980,15 +6146,16 @@ "link": true }, "node_modules/reflect.getprototypeof": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", - "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", "globalthis": "^1.0.3", "which-builtin-type": "^1.1.3" }, @@ -6006,14 +6173,15 @@ "dev": true }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -6187,13 +6355,13 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", - "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -6204,20 +6372,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-array-concat/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, "node_modules/safe-regex-test": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", - "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, "engines": { @@ -6293,30 +6455,32 @@ } }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -6344,14 +6508,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6508,34 +6676,51 @@ } }, "node_modules/string.prototype.matchall": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", - "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "regexp.prototype.flags": "^1.5.0", - "set-function-name": "^2.0.0", - "side-channel": "^1.0.4" + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -6545,28 +6730,31 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6767,29 +6955,30 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -6799,16 +6988,17 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -6818,14 +7008,20 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7463,13 +7659,13 @@ } }, "node_modules/which-builtin-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", - "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", "dev": true, "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.0.5", "is-finalizationregistry": "^1.0.2", @@ -7478,8 +7674,8 @@ "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -7488,38 +7684,35 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-builtin-type/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index fa87e057cb..930fac8a80 100644 --- a/package.json +++ b/package.json @@ -85,8 +85,8 @@ "eslint": "^8.55.0", "eslint-plugin-internal-playwright": "file:utils/eslint-plugin-internal-playwright", "eslint-plugin-notice": "^0.9.10", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.3.0", + "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react-hooks": "^4.6.2", "formidable": "^2.1.1", "license-checker": "^25.0.1", "mime": "^3.0.0", diff --git a/packages/html-reporter/src/icons.tsx b/packages/html-reporter/src/icons.tsx index 6111fac7c1..9609a2e23f 100644 --- a/packages/html-reporter/src/icons.tsx +++ b/packages/html-reporter/src/icons.tsx @@ -70,19 +70,19 @@ export const blank = () => { }; export const externalLink = () => { - return ; + return ; }; export const calendar = () => { - return ; + return ; }; export const person = () => { - return ; + return ; }; export const commit = () => { - return ; + return ; }; export const image = () => { diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx index 55d7d24a6c..419a8725ac 100644 --- a/packages/html-reporter/src/links.tsx +++ b/packages/html-reporter/src/links.tsx @@ -81,7 +81,7 @@ export const AttachmentLink: React.FunctionComponent<{ {attachment.path && {linkName || attachment.name}} {!attachment.path && {linkifyText(attachment.name)}} } loadChildren={attachment.body ? () => { - return [
{linkifyText(attachment.body!)}
]; + return [
{linkifyText(attachment.body!)}
]; } : undefined} depth={0} style={{ lineHeight: '32px' }}>; }; diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx index 4da49261d0..5bed3c8309 100644 --- a/packages/html-reporter/src/testCaseView.tsx +++ b/packages/html-reporter/src/testCaseView.tsx @@ -58,7 +58,7 @@ export const TestCaseView: React.FC<{ {labels && } } {!!visibleAnnotations.length && - {visibleAnnotations.map(annotation => )} + {visibleAnnotations.map((annotation, index) => )} } {test && ({ diff --git a/packages/playwright-core/bin/socks-certs/README.md b/packages/playwright-core/bin/socks-certs/README.md deleted file mode 100644 index 4950ef1f3c..0000000000 --- a/packages/playwright-core/bin/socks-certs/README.md +++ /dev/null @@ -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" -``` diff --git a/packages/playwright-core/bin/socks-certs/cert.pem b/packages/playwright-core/bin/socks-certs/cert.pem deleted file mode 100644 index cce2f57bd5..0000000000 --- a/packages/playwright-core/bin/socks-certs/cert.pem +++ /dev/null @@ -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----- diff --git a/packages/playwright-core/bin/socks-certs/key.pem b/packages/playwright-core/bin/socks-certs/key.pem deleted file mode 100644 index 75f8e3bccc..0000000000 --- a/packages/playwright-core/bin/socks-certs/key.pem +++ /dev/null @@ -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----- diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 2f987ac082..dd9fd60588 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -9,9 +9,9 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1249", + "revision": "1250", "installByDefault": false, - "browserVersion": "129.0.6654.0" + "browserVersion": "129.0.6658.0" }, { "name": "firefox", @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2061", + "revision": "2062", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 5991fcafd9..b0b72917cf 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -308,7 +308,7 @@ export class BrowserContext extends ChannelOwner } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise { - const source = await evaluationScript(script, arg); + const source = await evaluationScript(script, arg, arguments.length > 1); await this._channel.addInitScript({ source }); } @@ -552,13 +552,19 @@ function toAcceptDownloadsProtocol(acceptDownloads?: boolean) { export async function toClientCertificatesProtocol(certs?: BrowserContextOptions['clientCertificates']): Promise { if (!certs) return undefined; - return await Promise.all(certs.map(async cert => { - return { - origin: cert.origin, - cert: cert.certPath ? await fs.promises.readFile(cert.certPath) : undefined, - key: cert.keyPath ? await fs.promises.readFile(cert.keyPath) : undefined, - pfx: cert.pfxPath ? await fs.promises.readFile(cert.pfxPath) : undefined, - passphrase: cert.passphrase, - }; - })); + + const bufferizeContent = async (value?: Buffer, path?: string): Promise => { + if (value) + return value; + if (path) + return await fs.promises.readFile(path); + }; + + return await Promise.all(certs.map(async cert => ({ + origin: cert.origin, + cert: await bufferizeContent(cert.cert, cert.certPath), + key: await bufferizeContent(cert.key, cert.keyPath), + pfx: await bufferizeContent(cert.pfx, cert.pfxPath), + passphrase: cert.passphrase, + }))); } diff --git a/packages/playwright-core/src/client/clientHelper.ts b/packages/playwright-core/src/client/clientHelper.ts index 540230a4fc..fcc785b71b 100644 --- a/packages/playwright-core/src/client/clientHelper.ts +++ b/packages/playwright-core/src/client/clientHelper.ts @@ -28,20 +28,37 @@ export function envObjectToArray(env: types.Env): { name: string, value: string return result; } -export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise { +export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg: any, hasArg: boolean, addSourceUrl: boolean = true): Promise { if (typeof fun === 'function') { const source = fun.toString(); const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg); return `(${source})(${argString})`; } - if (arg !== undefined) - throw new Error('Cannot evaluate a string with arguments'); - if (isString(fun)) + if (isString(fun)) { + if (arg !== undefined) + throw new Error('Cannot evaluate a string with arguments'); return fun; - if (fun.content !== undefined) + } + if (fun.content !== undefined) { + if (arg !== undefined) + throw new Error('Cannot evaluate a string with arguments'); return fun.content; + } if (fun.path !== undefined) { let source = await fs.promises.readFile(fun.path, 'utf8'); + if (hasArg) { + // Assume a CJS module that has a function default export. + source = `(() => { + var exports = {}; var module = { exports }; + ${source} + let __pw_result__ = module.exports; + if (__pw_result__ && typeof __pw_result__ === 'object' && ('default' in __pw_result__)) + __pw_result__ = __pw_result__['default']; + if (typeof __pw_result__ !== 'function') + return __pw_result__; + return __pw_result__(${JSON.stringify(arg)}); + })()`; + } if (addSourceUrl) source = addSourceUrlToScript(source, fun.path); return source; diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index a10286fa9a..5ff6d6178d 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -492,7 +492,7 @@ export class Page extends ChannelOwner implements api.Page } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { - const source = await evaluationScript(script, arg); + const source = await evaluationScript(script, arg, arguments.length > 1); await this._channel.addInitScript({ source }); } diff --git a/packages/playwright-core/src/client/selectors.ts b/packages/playwright-core/src/client/selectors.ts index 2739be0e8d..c7a7967559 100644 --- a/packages/playwright-core/src/client/selectors.ts +++ b/packages/playwright-core/src/client/selectors.ts @@ -26,7 +26,7 @@ export class Selectors implements api.Selectors { private _registrations: channels.SelectorsRegisterParams[] = []; async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise { - const source = await evaluationScript(script, undefined, false); + const source = await evaluationScript(script, undefined, false, false); const params = { ...options, name, source }; for (const channel of this._channels) await channel._channel.register(params); diff --git a/packages/playwright-core/src/client/types.ts b/packages/playwright-core/src/client/types.ts index 8101257280..37d374e3ec 100644 --- a/packages/playwright-core/src/client/types.ts +++ b/packages/playwright-core/src/client/types.ts @@ -49,8 +49,11 @@ export const kLifecycleEvents: Set = new Set(['load', 'domconten export type ClientCertificate = { origin: string; + cert?: Buffer; certPath?: string; + key?: Buffer; keyPath?: string; + pfx?: Buffer; pfxPath?: string; passphrase?: string; }; diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 6367e8875e..a4d9cd9ee2 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -40,7 +40,7 @@ import { Tracing } from './trace/recorder/tracing'; import type * as types from './types'; import type { HeadersArray, ProxySettings } from './types'; import { kMaxCookieExpiresDateInSeconds } from './network'; -import { clientCertificatesToTLSOptions, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor'; +import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor'; type FetchRequestOptions = { userAgent: string; @@ -195,7 +195,7 @@ export abstract class APIRequestContext extends SdkObject { maxRedirects: params.maxRedirects === 0 ? -1 : params.maxRedirects === undefined ? 20 : params.maxRedirects, timeout, deadline, - ...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, requestUrl.origin), + ...getMatchingTLSOptionsForOrigin(this._defaultOptions().clientCertificates, requestUrl.origin), __testHookLookup: (params as any).__testHookLookup, }; // rejectUnauthorized = undefined is treated as true in Node.js 12. @@ -365,7 +365,7 @@ export abstract class APIRequestContext extends SdkObject { maxRedirects: options.maxRedirects - 1, timeout: options.timeout, deadline: options.deadline, - ...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, url.origin), + ...getMatchingTLSOptionsForOrigin(this._defaultOptions().clientCertificates, url.origin), __testHookLookup: options.__testHookLookup, }; // rejectUnauthorized = undefined is treated as true in node 12. diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 87569f00d6..931ba8ef73 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -659,18 +659,24 @@ export class Frame extends SdkObject { } url = helper.completeUserURL(url); - const sameDocument = helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (e: NavigationEvent) => !e.newDocument); - const navigateResult = await this._page._delegate.navigateFrame(this, url, referer); + const navigationEvents: NavigationEvent[] = []; + const collectNavigations = (arg: NavigationEvent) => navigationEvents.push(arg); + this.on(Frame.Events.InternalNavigation, collectNavigations); + const navigateResult = await this._page._delegate.navigateFrame(this, url, referer).finally( + () => this.off(Frame.Events.InternalNavigation, collectNavigations)); let event: NavigationEvent; if (navigateResult.newDocumentId) { - sameDocument.dispose(); - event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, (event: NavigationEvent) => { + const predicate = (event: NavigationEvent) => { // We are interested either in this specific document, or any other document that // did commit and replaced the expected document. return event.newDocument && (event.newDocument.documentId === navigateResult.newDocumentId || !event.error); - }).promise; - + }; + const events = navigationEvents.filter(predicate); + if (events.length) + event = events[0]; + else + event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, predicate).promise; if (event.newDocument!.documentId !== navigateResult.newDocumentId) { // This is just a sanity check. In practice, new navigation should // cancel the previous one and report "request cancelled"-like error. @@ -679,7 +685,13 @@ export class Frame extends SdkObject { if (event.error) throw event.error; } else { - event = await sameDocument.promise; + // Wait for same document navigation. + const predicate = (e: NavigationEvent) => !e.newDocument; + const events = navigationEvents.filter(predicate); + if (events.length) + event = events[0]; + else + event = await helper.waitForEvent(progress, this, Frame.Events.InternalNavigation, predicate).promise; } if (!this._firedLifecycleEvents.has(waitUntil)) diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index c9413e0a72..3432f159dd 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -552,28 +552,14 @@ class TextAssertionTool implements RecorderTool { private _recorder: Recorder; private _hoverHighlight: HighlightModel | null = null; private _action: actions.AssertAction | null = null; - private _dialogElement: HTMLElement | null = null; - private _acceptButton: HTMLElement; - private _cancelButton: HTMLElement; - private _keyboardListener: ((event: KeyboardEvent) => void) | undefined; + private _dialog: Dialog; private _textCache = new Map(); private _kind: 'text' | 'value'; constructor(recorder: Recorder, kind: 'text' | 'value') { this._recorder = recorder; this._kind = kind; - - this._acceptButton = this._recorder.document.createElement('x-pw-tool-item'); - this._acceptButton.title = 'Accept'; - this._acceptButton.classList.add('accept'); - this._acceptButton.appendChild(this._recorder.document.createElement('x-div')); - this._acceptButton.addEventListener('click', () => this._commit()); - - this._cancelButton = this._recorder.document.createElement('x-pw-tool-item'); - this._cancelButton.title = 'Close'; - this._cancelButton.classList.add('cancel'); - this._cancelButton.appendChild(this._recorder.document.createElement('x-div')); - this._cancelButton.addEventListener('click', () => this._closeDialog()); + this._dialog = new Dialog(recorder); } cursor() { @@ -581,7 +567,7 @@ class TextAssertionTool implements RecorderTool { } cleanup() { - this._closeDialog(); + this._dialog.close(); this._hoverHighlight = null; } @@ -590,7 +576,7 @@ class TextAssertionTool implements RecorderTool { if (this._kind === 'value') { this._commitAssertValue(); } else { - if (!this._dialogElement) + if (!this._dialog.isShowing()) this._showDialog(); } } @@ -611,7 +597,7 @@ class TextAssertionTool implements RecorderTool { } onMouseMove(event: MouseEvent) { - if (this._dialogElement) + if (this._dialog.isShowing()) return; const target = this._recorder.deepEventTarget(event); if (this._hoverHighlight?.elements[0] === target) @@ -691,9 +677,9 @@ class TextAssertionTool implements RecorderTool { } private _commit() { - if (!this._action || !this._dialogElement) + if (!this._action || !this._dialog.isShowing()) return; - this._closeDialog(); + this._dialog.close(); this._recorder.delegate.recordAction?.(this._action); this._recorder.delegate.setMode?.('recording'); } @@ -705,31 +691,6 @@ class TextAssertionTool implements RecorderTool { if (!this._action || this._action.name !== 'assertText') return; - this._dialogElement = this._recorder.document.createElement('x-pw-dialog'); - this._keyboardListener = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - this._closeDialog(); - return; - } - if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { - if (this._dialogElement) - this._commit(); - return; - } - }; - - this._recorder.document.addEventListener('keydown', this._keyboardListener, true); - const toolbarElement = this._recorder.document.createElement('x-pw-tools-list'); - const labelElement = this._recorder.document.createElement('label'); - labelElement.textContent = 'Assert that element contains text'; - toolbarElement.appendChild(labelElement); - toolbarElement.appendChild(this._recorder.document.createElement('x-spacer')); - toolbarElement.appendChild(this._acceptButton); - toolbarElement.appendChild(this._cancelButton); - - this._dialogElement.appendChild(toolbarElement); - const bodyElement = this._recorder.document.createElement('x-pw-dialog-body'); - const action = this._action; const textElement = this._recorder.document.createElement('textarea'); textElement.setAttribute('spellcheck', 'false'); @@ -747,24 +708,18 @@ class TextAssertionTool implements RecorderTool { textElement.classList.toggle('does-not-match', !matches); }; textElement.addEventListener('input', updateAndValidate); - bodyElement.appendChild(textElement); - this._dialogElement.appendChild(bodyElement); - this._recorder.highlight.appendChild(this._dialogElement); - const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, this._dialogElement); - this._dialogElement.style.top = position.anchorTop + 'px'; - this._dialogElement.style.left = position.anchorLeft + 'px'; + const label = 'Assert that element contains text'; + const dialogElement = this._dialog.show({ + label, + body: textElement, + onCommit: () => this._commit(), + }); + const position = this._recorder.highlight.tooltipPosition(this._recorder.highlight.firstBox()!, dialogElement); + this._dialog.moveTo(position.anchorTop, position.anchorLeft); textElement.focus(); } - private _closeDialog() { - if (!this._dialogElement) - return; - this._dialogElement.remove(); - this._recorder.document.removeEventListener('keydown', this._keyboardListener!); - this._dialogElement = null; - } - private _commitAssertValue() { if (this._kind !== 'value') return; @@ -1219,6 +1174,87 @@ export class Recorder { } } +class Dialog { + private _recorder: Recorder; + private _dialogElement: HTMLElement | null = null; + private _keyboardListener: ((event: KeyboardEvent) => void) | undefined; + + constructor(recorder: Recorder) { + this._recorder = recorder; + } + + isShowing(): boolean { + return !!this._dialogElement; + } + + show(options: { + label: string; + body: Element; + onCommit: () => void; + onCancel?: () => void; + }) { + const acceptButton = this._recorder.document.createElement('x-pw-tool-item'); + acceptButton.title = 'Accept'; + acceptButton.classList.add('accept'); + acceptButton.appendChild(this._recorder.document.createElement('x-div')); + acceptButton.addEventListener('click', () => options.onCommit()); + + const cancelButton = this._recorder.document.createElement('x-pw-tool-item'); + cancelButton.title = 'Close'; + cancelButton.classList.add('cancel'); + cancelButton.appendChild(this._recorder.document.createElement('x-div')); + cancelButton.addEventListener('click', () => { + this.close(); + options.onCancel?.(); + }); + + this._dialogElement = this._recorder.document.createElement('x-pw-dialog'); + this._keyboardListener = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + this.close(); + options.onCancel?.(); + return; + } + if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { + if (this._dialogElement) + options.onCommit(); + return; + } + }; + + this._recorder.document.addEventListener('keydown', this._keyboardListener, true); + const toolbarElement = this._recorder.document.createElement('x-pw-tools-list'); + const labelElement = this._recorder.document.createElement('label'); + labelElement.textContent = options.label; + toolbarElement.appendChild(labelElement); + toolbarElement.appendChild(this._recorder.document.createElement('x-spacer')); + toolbarElement.appendChild(acceptButton); + toolbarElement.appendChild(cancelButton); + + this._dialogElement.appendChild(toolbarElement); + const bodyElement = this._recorder.document.createElement('x-pw-dialog-body'); + bodyElement.appendChild(options.body); + this._dialogElement.appendChild(bodyElement); + this._recorder.highlight.appendChild(this._dialogElement); + return this._dialogElement; + } + + moveTo(top: number, left: number) { + if (!this._dialogElement) + return; + this._dialogElement.style.top = top + 'px'; + this._dialogElement.style.left = left + 'px'; + } + + close() { + if (!this._dialogElement) + return; + this._dialogElement.remove(); + this._recorder.document.removeEventListener('keydown', this._keyboardListener!); + this._dialogElement = null; + } +} + function deepActiveElement(document: Document): Element | null { let activeElement = document.activeElement; while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement) diff --git a/packages/playwright-core/src/server/injected/roleUtils.ts b/packages/playwright-core/src/server/injected/roleUtils.ts index 57681e0542..498fc189aa 100644 --- a/packages/playwright-core/src/server/injected/roleUtils.ts +++ b/packages/playwright-core/src/server/injected/roleUtils.ts @@ -261,12 +261,16 @@ function getAriaBoolean(attr: string | null) { return attr === null ? undefined : attr.toLowerCase() === 'true'; } +function isElementIgnoredForAria(element: Element) { + return ['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element)); +} + // https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion, but including "none" and "presentation" roles // Not implemented: // `Any descendants of elements that have the characteristic "Children Presentational: True"` // https://www.w3.org/TR/wai-aria-1.2/#aria-hidden export function isElementHiddenForAria(element: Element): boolean { - if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element))) + if (isElementIgnoredForAria(element)) return true; const style = getElementComputedStyle(element); const isSlot = element.nodeName === 'SLOT'; @@ -371,7 +375,8 @@ function getPseudoContent(element: Element, pseudo: '::before' | '::after') { } function getPseudoContentImpl(pseudoStyle: CSSStyleDeclaration | undefined) { - if (!pseudoStyle) + // Note: all browsers ignore display:none and visibility:hidden pseudos. + if (!pseudoStyle || pseudoStyle.display === 'none' || pseudoStyle.visibility === 'hidden') return ''; const content = pseudoStyle.content; if ((content[0] === '\'' && content[content.length - 1] === '\'') || @@ -496,14 +501,17 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt // step 2a. Hidden Not Referenced: If the current node is hidden and is: // Not part of an aria-labelledby or aria-describedby traversal, where the node directly referenced by that relation was hidden. // Nor part of a native host language text alternative element (e.g. label in HTML) or attribute traversal, where the root of that traversal was hidden. - if (!options.includeHidden && - !options.embeddedInLabelledBy?.hidden && - !options.embeddedInDescribedBy?.hidden && - !options?.embeddedInNativeTextAlternative?.hidden && - !options?.embeddedInLabel?.hidden && - isElementHiddenForAria(element)) { - options.visitedElements.add(element); - return ''; + if (!options.includeHidden) { + const isEmbeddedInHiddenReferenceTraversal = + !!options.embeddedInLabelledBy?.hidden || + !!options.embeddedInDescribedBy?.hidden || + !!options.embeddedInNativeTextAlternative?.hidden || + !!options.embeddedInLabel?.hidden; + if (isElementIgnoredForAria(element) || + (!isEmbeddedInHiddenReferenceTraversal && isElementHiddenForAria(element))) { + options.visitedElements.add(element); + return ''; + } } const labelledBy = getAriaLabelledByElements(element); diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index 4e371df201..32a7b1cebd 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -15,14 +15,12 @@ */ import net from 'net'; -import path from 'path'; import http2 from 'http2'; import type https from 'https'; -import fs from 'fs'; import tls from 'tls'; import stream from 'stream'; import { createSocket, createTLSSocket } from '../utils/happy-eyeballs'; -import { escapeHTML, ManualPromise, rewriteErrorMessage } from '../utils'; +import { escapeHTML, generateSelfSignedCertificate, ManualPromise, rewriteErrorMessage } from '../utils'; import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../common/socksProxy'; import { SocksProxy } from '../common/socksProxy'; import type * as channels from '@protocol/channels'; @@ -32,10 +30,8 @@ let dummyServerTlsOptions: tls.TlsOptions | undefined = undefined; function loadDummyServerCertsIfNeeded() { if (dummyServerTlsOptions) return; - dummyServerTlsOptions = { - key: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/key.pem')), - cert: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/cert.pem')), - }; + const { cert, key } = generateSelfSignedCertificate(); + dummyServerTlsOptions = { key, cert }; } class ALPNCache { @@ -161,7 +157,6 @@ class SocksProxyConnection { let targetTLS: tls.TLSSocket | undefined = undefined; const handleError = (error: Error) => { - error = rewriteOpenSSLErrorIfNeeded(error); debugLogger.log('client-certificates', `error when connecting to target: ${error.message.replaceAll('\n', ' ')}`); const responseBody = escapeHTML('Playwright client-certificate error: ' + error.message) .replaceAll('\n', '
'); @@ -202,14 +197,6 @@ class SocksProxyConnection { } }; - let secureContext: tls.SecureContext; - try { - secureContext = tls.createSecureContext(clientCertificatesToTLSOptions(this.socksProxy.clientCertificates, new URL(`https://${this.host}:${this.port}`).origin)); - } catch (error) { - handleError(error); - return; - } - if (this._closed) { internalTLS.destroy(); return; @@ -221,7 +208,7 @@ class SocksProxyConnection { rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors, ALPNProtocols: [internalTLS.alpnProtocol || 'http/1.1'], servername: !net.isIP(this.host) ? this.host : undefined, - secureContext, + secureContext: this.socksProxy.secureContextMap.get(new URL(`https://${this.host}:${this.port}`).origin), }); targetTLS.once('secureConnect', () => { @@ -240,7 +227,7 @@ export class ClientCertificatesProxy { _socksProxy: SocksProxy; private _connections: Map = new Map(); ignoreHTTPSErrors: boolean | undefined; - clientCertificates: channels.BrowserNewContextOptions['clientCertificates']; + secureContextMap: Map = new Map(); alpnCache: ALPNCache; constructor( @@ -248,7 +235,7 @@ export class ClientCertificatesProxy { ) { this.alpnCache = new ALPNCache(); this.ignoreHTTPSErrors = contextOptions.ignoreHTTPSErrors; - this.clientCertificates = contextOptions.clientCertificates; + this._initSecureContexts(contextOptions.clientCertificates); this._socksProxy = new SocksProxy(); this._socksProxy.setPattern('*'); this._socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => { @@ -270,6 +257,27 @@ export class ClientCertificatesProxy { loadDummyServerCertsIfNeeded(); } + _initSecureContexts(clientCertificates: channels.BrowserNewContextOptions['clientCertificates']) { + // Step 1. Group certificates by origin. + const origin2certs = new Map(); + 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 { const port = await this._socksProxy.listen(0, '127.0.0.1'); return `socks5://127.0.0.1:${port}`; @@ -280,25 +288,25 @@ export class ClientCertificatesProxy { } } -export function clientCertificatesToTLSOptions( - clientCertificates: channels.BrowserNewContextOptions['clientCertificates'], - origin: string +function normalizeOrigin(origin: string): string { + try { + return new URL(origin).origin; + } catch (error) { + return origin; + } +} + +function convertClientCertificatesToTLSOptions( + clientCertificates: channels.BrowserNewContextOptions['clientCertificates'] ): Pick | undefined { - const matchingCerts = clientCertificates?.filter(c => { - try { - return new URL(c.origin).origin === origin; - } catch (error) { - return c.origin === origin; - } - }); - if (!matchingCerts || !matchingCerts.length) + if (!clientCertificates || !clientCertificates.length) return; const tlsOptions = { pfx: [] as { buf: Buffer, passphrase?: string }[], key: [] as { pem: Buffer, passphrase?: string }[], cert: [] as Buffer[], }; - for (const cert of matchingCerts) { + for (const cert of clientCertificates) { if (cert.cert) tlsOptions.cert.push(cert.cert); if (cert.key) @@ -309,6 +317,16 @@ export function clientCertificatesToTLSOptions( return tlsOptions; } +export function getMatchingTLSOptionsForOrigin( + clientCertificates: channels.BrowserNewContextOptions['clientCertificates'], + origin: string +): Pick | undefined { + const matchingCerts = clientCertificates?.filter(c => + normalizeOrigin(c.origin) === origin + ); + return convertClientCertificatesToTLSOptions(matchingCerts); +} + function rewriteToLocalhostIfNeeded(host: string): string { return host === 'local.playwright' ? 'localhost' : host; } diff --git a/packages/playwright-core/src/utils/crypto.ts b/packages/playwright-core/src/utils/crypto.ts index f3e47f6993..5da56d4e9b 100644 --- a/packages/playwright-core/src/utils/crypto.ts +++ b/packages/playwright-core/src/utils/crypto.ts @@ -15,6 +15,7 @@ */ import crypto from 'crypto'; +import { assert } from './debug'; export function createGuid(): string { return crypto.randomBytes(16).toString('hex'); @@ -25,3 +26,170 @@ export function calculateSha1(buffer: Buffer | string): string { hash.update(buffer); return hash.digest('hex'); } + +// Variable-length quantity encoding aka. base-128 encoding +function encodeBase128(value: number): Buffer { + const bytes = []; + do { + let byte = value & 0x7f; + value >>>= 7; + if (bytes.length > 0) byte |= 0x80; + bytes.push(byte); + } while (value > 0); + return Buffer.from(bytes.reverse()); +}; + +// ASN1/DER Speficiation: https://www.itu.int/rec/T-REC-X.680-X.693-202102-I/en +class DER { + static encodeSequence(data: Buffer[]): Buffer { + return this._encode(0x30, Buffer.concat(data)); + } + static encodeInteger(data: number): Buffer { + assert(data >= -128 && data <= 127); + return this._encode(0x02, Buffer.from([data])); + } + static encodeObjectIdentifier(oid: string): Buffer { + const parts = oid.split('.').map((v) => Number(v)); + // Encode the second part, which could be large, using base-128 encoding if necessary + const output = [encodeBase128(40 * parts[0] + parts[1])]; + + for (let i = 2; i < parts.length; i++) { + output.push(encodeBase128(parts[i])); + } + + return this._encode(0x06, Buffer.concat(output)); + } + static encodeNull(): Buffer { + return Buffer.from([0x05, 0x00]); + } + static encodeSet(data: Buffer[]): Buffer { + assert(data.length === 1, 'Only one item in the set is supported. We\'d need to sort the data to support more.'); + // We expect the data to be already sorted. + return this._encode(0x31, Buffer.concat(data)); + } + static encodeExplicitContextDependent(tag: number, data: Buffer): Buffer { + return this._encode(0xa0 + tag, data); + } + static encodePrintableString(data: string): Buffer { + return this._encode(0x13, Buffer.from(data)); + } + static encodeBitString(data: Buffer): Buffer { + // The first byte of the content is the number of unused bits at the end + const unusedBits = 0; // Assuming all bits are used + const content = Buffer.concat([Buffer.from([unusedBits]), data]); + return this._encode(0x03, content); + } + static encodeDate(date: Date): Buffer { + const year = date.getUTCFullYear(); + const isGeneralizedTime = year >= 2050; + const parts = [ + isGeneralizedTime ? year.toString() : year.toString().slice(-2), + (date.getUTCMonth() + 1).toString().padStart(2, '0'), + date.getUTCDate().toString().padStart(2, '0'), + date.getUTCHours().toString().padStart(2, '0'), + date.getUTCMinutes().toString().padStart(2, '0'), + date.getUTCSeconds().toString().padStart(2, '0') + ]; + const encodedDate = parts.join('') + 'Z'; + const tag = isGeneralizedTime ? 0x18 : 0x17; // 0x18 for GeneralizedTime, 0x17 for UTCTime + return this._encode(tag, Buffer.from(encodedDate)); + } + private static _encode(tag: number, data: Buffer): Buffer { + const lengthBytes = this._encodeLength(data.length); + return Buffer.concat([Buffer.from([tag]), lengthBytes, data]); + } + private static _encodeLength(length: number): Buffer { + if (length < 128) { + return Buffer.from([length]); + } else { + const lengthBytes = []; + while (length > 0) { + lengthBytes.unshift(length & 0xFF); + length >>= 8; + } + return Buffer.from([0x80 | lengthBytes.length, ...lengthBytes]); + } + } +} + +// X.509 Specification: https://datatracker.ietf.org/doc/html/rfc2459#section-4.1 +export function generateSelfSignedCertificate() { + const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }); + const publicKeyDer = publicKey.export({ type: 'pkcs1', format: 'der' }); + + const oneYearInMilliseconds = 365 * 24 * 60 * 60 * 1_000; + const notBefore = new Date(new Date().getTime() - oneYearInMilliseconds); + const notAfter = new Date(new Date().getTime() + oneYearInMilliseconds); + + // List of fields / structure: https://datatracker.ietf.org/doc/html/rfc2459#section-4.1 + const tbsCertificate = DER.encodeSequence([ + DER.encodeExplicitContextDependent(0, DER.encodeInteger(1)), // version + DER.encodeInteger(1), // serialNumber + DER.encodeSequence([ + DER.encodeObjectIdentifier('1.2.840.113549.1.1.11'), // sha256WithRSAEncryption PKCS #1 + DER.encodeNull() + ]), // signature + DER.encodeSequence([ + DER.encodeSet([ + DER.encodeSequence([ + DER.encodeObjectIdentifier('2.5.4.3'), // commonName X.520 DN component + DER.encodePrintableString('localhost') + ]), + ]), + DER.encodeSet([ + DER.encodeSequence([ + DER.encodeObjectIdentifier('2.5.4.10'), // organizationName X.520 DN component + DER.encodePrintableString('Playwright Client Certificate Support') + ]) + ]) + ]), // issuer + DER.encodeSequence([ + DER.encodeDate(notBefore), // notBefore + DER.encodeDate(notAfter), // notAfter + ]), // validity + DER.encodeSequence([ + DER.encodeSet([ + DER.encodeSequence([ + DER.encodeObjectIdentifier('2.5.4.3'), // commonName X.520 DN component + DER.encodePrintableString('localhost') + ]), + ]), + DER.encodeSet([ + DER.encodeSequence([ + DER.encodeObjectIdentifier('2.5.4.10'), // organizationName X.520 DN component + DER.encodePrintableString('Playwright Client Certificate Support') + ]) + ]) + ]), // subject + DER.encodeSequence([ + DER.encodeSequence([ + DER.encodeObjectIdentifier('1.2.840.113549.1.1.1'), // rsaEncryption PKCS #1 + DER.encodeNull() + ]), + DER.encodeBitString(publicKeyDer) + ]), // SubjectPublicKeyInfo + ]); + + const signature = crypto.sign('sha256', tbsCertificate, privateKey); + + const certificate = DER.encodeSequence([ + tbsCertificate, + DER.encodeSequence([ + DER.encodeObjectIdentifier('1.2.840.113549.1.1.11'), // sha256WithRSAEncryption PKCS #1 + DER.encodeNull() + ]), + DER.encodeBitString(signature) + ]); + + const certPem = [ + '-----BEGIN CERTIFICATE-----', + // Split the base64 string into lines of 64 characters + certificate.toString('base64').match(/.{1,64}/g)!.join('\n'), + '-----END CERTIFICATE-----' + ].join('\n'); + + return { + cert: certPem, + key: privateKey.export({ type: 'pkcs1', format: 'pem' }), + }; +} diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index bf4e35a9ca..0d018dcfd3 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -288,8 +288,41 @@ export interface Page { * [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script) * and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not * defined. + * + * **Bundling** + * + * If you have a complex script split into several files, it needs to be bundled into a single file first. We + * recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a + * commonjs module and pass `path` and `arg`. + * + * ```js + * // mocks/mockRandom.ts + * // This script can import other files. + * import { defaultValue } from './defaultValue'; + * + * export default function(value?: number) { + * window.Math.random = () => value ?? defaultValue; + * } + * ``` + * + * ```js + * // tests/example.spec.ts + * const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') }; + * + * // Passing 42 as an argument to the default export function. + * await page.addInitScript({ path: mockPath }, 42); + * + * // Make sure to pass undefined even if you do not need to pass an argument. + * // This instructs Playwright to treat the file as a commonjs module. + * await page.addInitScript({ path: mockPath }, undefined); + * ``` + * * @param script Script to be evaluated in the page. - * @param arg Optional argument to pass to `script` (only supported when passing a function). + * @param arg Optional JSON-serializable argument to pass to `script`. + * - When `script` is a function, the argument is passed to it directly. + * - When `script` is a file path, the file is assumed to be a commonjs module. The default export, either + * `module.exports` or `module.exports.default`, should be a function that's going to be executed with this + * argument. */ addInitScript(script: PageFunction | { path?: string, content?: string }, arg?: Arg): Promise; @@ -7666,8 +7699,41 @@ export interface BrowserContext { * [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script) * and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not * defined. + * + * **Bundling** + * + * If you have a complex script split into several files, it needs to be bundled into a single file first. We + * recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a + * commonjs module and pass `path` and `arg`. + * + * ```js + * // mocks/mockRandom.ts + * // This script can import other files. + * import { defaultValue } from './defaultValue'; + * + * export default function(value?: number) { + * window.Math.random = () => value ?? defaultValue; + * } + * ``` + * + * ```js + * // tests/example.spec.ts + * const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') }; + * + * // Passing 42 as an argument to the default export function. + * await context.addInitScript({ path: mockPath }, 42); + * + * // Make sure to pass undefined even if you do not need to pass an argument. + * // This instructs Playwright to treat the file as a commonjs module. + * await context.addInitScript({ path: mockPath }, undefined); + * ``` + * * @param script Script to be evaluated in all pages in the browser context. - * @param arg Optional argument to pass to `script` (only supported when passing a function). + * @param arg Optional JSON-serializable argument to pass to `script`. + * - When `script` is a function, the argument is passed to it directly. + * - When `script` is a file path, the file is assumed to be a commonjs module. The default export, either + * `module.exports` or `module.exports.default`, should be a function that's going to be executed with this + * argument. */ addInitScript(script: PageFunction | { path?: string, content?: string }, arg?: Arg): Promise; @@ -9138,10 +9204,10 @@ export interface Browser { * * **Details** * - * An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a - * single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the - * certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that - * the certificate is valid for. + * An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`, + * a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally, + * `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided + * with an exact match to the request origin that the certificate is valid for. * * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. * @@ -9159,16 +9225,31 @@ export interface Browser { */ certPath?: string; + /** + * Direct value of the certificate in PEM format. + */ + cert?: Buffer; + /** * Path to the file with the private key in PEM format. */ keyPath?: string; + /** + * Direct value of the private key in PEM format. + */ + key?: Buffer; + /** * Path to the PFX or PKCS12 encoded private key and certificate chain. */ pfxPath?: string; + /** + * Direct value of the PFX or PKCS12 encoded private key and certificate chain. + */ + pfx?: Buffer; + /** * Passphrase for the private key (PEM or PFX). */ @@ -13850,10 +13931,10 @@ export interface BrowserType { * * **Details** * - * An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a - * single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the - * certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that - * the certificate is valid for. + * An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`, + * a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally, + * `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided + * with an exact match to the request origin that the certificate is valid for. * * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. * @@ -13871,16 +13952,31 @@ export interface BrowserType { */ certPath?: string; + /** + * Direct value of the certificate in PEM format. + */ + cert?: Buffer; + /** * Path to the file with the private key in PEM format. */ keyPath?: string; + /** + * Direct value of the private key in PEM format. + */ + key?: Buffer; + /** * Path to the PFX or PKCS12 encoded private key and certificate chain. */ pfxPath?: string; + /** + * Direct value of the PFX or PKCS12 encoded private key and certificate chain. + */ + pfx?: Buffer; + /** * Passphrase for the private key (PEM or PFX). */ @@ -16259,10 +16355,10 @@ export interface APIRequest { * * **Details** * - * An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a - * single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the - * certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that - * the certificate is valid for. + * An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`, + * a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally, + * `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided + * with an exact match to the request origin that the certificate is valid for. * * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. * @@ -16280,16 +16376,31 @@ export interface APIRequest { */ certPath?: string; + /** + * Direct value of the certificate in PEM format. + */ + cert?: Buffer; + /** * Path to the file with the private key in PEM format. */ keyPath?: string; + /** + * Direct value of the private key in PEM format. + */ + key?: Buffer; + /** * Path to the PFX or PKCS12 encoded private key and certificate chain. */ pfxPath?: string; + /** + * Direct value of the PFX or PKCS12 encoded private key and certificate chain. + */ + pfx?: Buffer; + /** * Passphrase for the private key (PEM or PFX). */ @@ -20600,10 +20711,10 @@ export interface BrowserContextOptions { * * **Details** * - * An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a - * single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the - * certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that - * the certificate is valid for. + * An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`, + * a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally, + * `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided + * with an exact match to the request origin that the certificate is valid for. * * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. * @@ -20621,16 +20732,31 @@ export interface BrowserContextOptions { */ certPath?: string; + /** + * Direct value of the certificate in PEM format. + */ + cert?: Buffer; + /** * Path to the file with the private key in PEM format. */ keyPath?: string; + /** + * Direct value of the private key in PEM format. + */ + key?: Buffer; + /** * Path to the PFX or PKCS12 encoded private key and certificate chain. */ pfxPath?: string; + /** + * Direct value of the PFX or PKCS12 encoded private key and certificate chain. + */ + pfx?: Buffer; + /** * Passphrase for the private key (PEM or PFX). */ diff --git a/packages/playwright-ct-vue/index.d.ts b/packages/playwright-ct-vue/index.d.ts index 2d182d4588..822711f4b6 100644 --- a/packages/playwright-ct-vue/index.d.ts +++ b/packages/playwright-ct-vue/index.d.ts @@ -55,7 +55,7 @@ export interface MountResultJsx extends Locator { export const test: TestType<{ mount( component: JSX.Element, - options: MountOptionsJsx + options?: MountOptionsJsx ): Promise; mount( component: Component, diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 8b392afa37..130e731b5a 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -248,6 +248,11 @@ const playwrightFixtures: Fixtures = ({ }, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any], _setupArtifacts: [async ({ playwright, screenshot }, use, testInfo) => { + // This fixture has a separate zero-timeout slot to ensure that artifact collection + // happens even after some fixtures or hooks time out. + // Now that default test timeout is known, we can replace zero with an actual value. + testInfo.setTimeout(testInfo.project.timeout); + const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot); await artifactsRecorder.willStartTest(testInfo as TestInfoImpl); const csiListener: ClientInstrumentationListener = { @@ -297,7 +302,7 @@ const playwrightFixtures: Fixtures = ({ clientInstrumentation.removeListener(csiListener); await artifactsRecorder.didFinishTest(); - }, { auto: 'all-hooks-included', title: 'trace recording', box: true } as any], + }, { auto: 'all-hooks-included', title: 'trace recording', box: true, timeout: 0 } as any], _contextFactory: [async ({ browser, video, _reuseContext, _combinedContextOptions /** mitigate dep-via-auto lack of traceability */ }, use, testInfo) => { const testInfoImpl = testInfo as TestInfoImpl; diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index aa2893b250..0a1e001a91 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -63,7 +63,7 @@ export class TestRun { export function createTaskRunner(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner { const taskRunner = TaskRunner.create(reporters, config.config.globalTimeout); addGlobalSetupTasks(taskRunner, config); - taskRunner.addTask('load tests', createLoadTask('in-process', { filterOnly: true, filterOnlyChanged: true, failOnLoadErrors: true })); + taskRunner.addTask('load tests', createLoadTask('in-process', { filterOnly: true, failOnLoadErrors: true })); addRunTasks(taskRunner, config); return taskRunner; } @@ -76,14 +76,14 @@ export function createTaskRunnerForWatchSetup(config: FullConfigInternal, report export function createTaskRunnerForWatch(config: FullConfigInternal, reporters: ReporterV2[], additionalFileMatcher?: Matcher): TaskRunner { const taskRunner = TaskRunner.create(reporters); - taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, filterOnlyChanged: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true, additionalFileMatcher })); + taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true, additionalFileMatcher })); addRunTasks(taskRunner, config); return taskRunner; } export function createTaskRunnerForTestServer(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner { const taskRunner = TaskRunner.create(reporters); - taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, filterOnlyChanged: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true })); + taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true })); addRunTasks(taskRunner, config); return taskRunner; } @@ -108,7 +108,7 @@ function addRunTasks(taskRunner: TaskRunner, config: FullConfigInternal export function createTaskRunnerForList(config: FullConfigInternal, reporters: ReporterV2[], mode: 'in-process' | 'out-of-process', options: { failOnLoadErrors: boolean }): TaskRunner { const taskRunner = TaskRunner.create(reporters, config.config.globalTimeout); - taskRunner.addTask('load tests', createLoadTask(mode, { ...options, filterOnly: false, filterOnlyChanged: false })); + taskRunner.addTask('load tests', createLoadTask(mode, { ...options, filterOnly: false })); taskRunner.addTask('report begin', createReportBeginTask()); return taskRunner; } @@ -222,14 +222,14 @@ function createListFilesTask(): Task { }; } -function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, filterOnlyChanged: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task { +function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task { return { setup: async (reporter, testRun, errors, softErrors) => { await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter, options.additionalFileMatcher); await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors); let cliOnlyChangedMatcher: Matcher | undefined = undefined; - if (testRun.config.cliOnlyChanged && options.filterOnlyChanged) { + if (testRun.config.cliOnlyChanged) { for (const plugin of testRun.config.plugins) await plugin.instance?.populateDependencies?.(); const changedFiles = await detectChangedTestFiles(testRun.config.cliOnlyChanged, testRun.config.configDir); diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 28bc7798b5..0131cb16c9 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -5206,10 +5206,10 @@ export interface PlaywrightTestOptions { * * **Details** * - * An array of client certificates to be used. Each certificate object must have both `certPath` and `keyPath` or a - * single `pfxPath` to load the client certificate. Optionally, `passphrase` property should be provided if the - * certficiate is encrypted. The `origin` property should be provided with an exact match to the request origin that - * the certificate is valid for. + * An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`, + * a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally, + * `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided + * with an exact match to the request origin that the certificate is valid for. * * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. * diff --git a/packages/recorder/src/recorder.tsx b/packages/recorder/src/recorder.tsx index b3966fd01a..486aeae701 100644 --- a/packages/recorder/src/recorder.tsx +++ b/packages/recorder/src/recorder.tsx @@ -171,7 +171,7 @@ export const Recorder: React.FC = ({ sidebarSize={200} main={} sidebar={ copy(locator)} />] : []} + rightToolbar={selectedTab === 'locator' ? [ copy(locator)} />] : []} tabs={[ { id: 'locator', diff --git a/packages/trace-viewer/src/snapshotServer.ts b/packages/trace-viewer/src/snapshotServer.ts index c3c2f5a624..b1dd371cb3 100644 --- a/packages/trace-viewer/src/snapshotServer.ts +++ b/packages/trace-viewer/src/snapshotServer.ts @@ -85,7 +85,9 @@ export class SnapshotServer { contentType = `${contentType}; charset=utf-8`; const headers = new Headers(); - headers.set('Content-Type', contentType); + // "x-unknown" in the har means "no content type". + if (contentType !== 'x-unknown') + headers.set('Content-Type', contentType); for (const { name, value } of resource.response.headers) headers.set(name, value); headers.delete('Content-Encoding'); diff --git a/packages/trace-viewer/src/traceModel.ts b/packages/trace-viewer/src/traceModel.ts index 0fc1a73efa..1248dde967 100644 --- a/packages/trace-viewer/src/traceModel.ts +++ b/packages/trace-viewer/src/traceModel.ts @@ -114,9 +114,11 @@ export class TraceModel { async resourceForSha1(sha1: string): Promise { const blob = await this._backend.readBlob('resources/' + sha1); - if (!blob) - return; - return new Blob([blob], { type: this._resourceToContentType.get(sha1) || 'application/octet-stream' }); + const contentType = this._resourceToContentType.get(sha1); + // "x-unknown" in the har means "no content type". + if (!blob || contentType === undefined || contentType === 'x-unknown') + return blob; + return new Blob([blob], { type: contentType }); } storage(): SnapshotStorage { diff --git a/packages/trace-viewer/src/ui/attachmentsTab.tsx b/packages/trace-viewer/src/ui/attachmentsTab.tsx index 8c72c7fee3..8b6cefe14d 100644 --- a/packages/trace-viewer/src/ui/attachmentsTab.tsx +++ b/packages/trace-viewer/src/ui/attachmentsTab.tsx @@ -126,7 +126,7 @@ export const AttachmentsTab: React.FunctionComponent<{ const url = attachmentURL(a); return ; })} {attachments.size ?
Attachments
: undefined} diff --git a/packages/trace-viewer/src/ui/consoleTab.tsx b/packages/trace-viewer/src/ui/consoleTab.tsx index a7b8318386..b2947f5011 100644 --- a/packages/trace-viewer/src/ui/consoleTab.tsx +++ b/packages/trace-viewer/src/ui/consoleTab.tsx @@ -213,6 +213,7 @@ function format(args: { preview: string, value: any }[]): JSX.Element[] { } function formatAnsi(text: string): JSX.Element[] { + // eslint-disable-next-line react/jsx-key return []; } diff --git a/packages/trace-viewer/src/ui/networkFilters.tsx b/packages/trace-viewer/src/ui/networkFilters.tsx index de6c827e2b..a88332e7c6 100644 --- a/packages/trace-viewer/src/ui/networkFilters.tsx +++ b/packages/trace-viewer/src/ui/networkFilters.tsx @@ -26,10 +26,10 @@ export type FilterState = { export const defaultFilterState: FilterState = { searchValue: '', resourceType: 'All' }; -export const NetworkFilters: React.FunctionComponent<{ +export const NetworkFilters = ({ filterState, onFilterStateChange }: { filterState: FilterState, onFilterStateChange: (filterState: FilterState) => void, -}> = ({ filterState, onFilterStateChange }) => { +}) => { return (
]} + leftToolbar={[]} tabs={[ { id: 'request', @@ -101,12 +101,13 @@ const ResponseTab: React.FunctionComponent<{ const BodyTab: React.FunctionComponent<{ resource: ResourceSnapshot; }> = ({ resource }) => { - const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, mimeType?: string } | null>(null); + const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, mimeType?: string, font?: BinaryData } | null>(null); React.useEffect(() => { const readResources = async () => { if (resource.response.content._sha1) { const useBase64 = resource.response.content.mimeType.includes('image'); + const isFont = resource.response.content.mimeType.includes('font'); const response = await fetch(`sha1/${resource.response.content._sha1}`); if (useBase64) { const blob = await response.blob(); @@ -114,6 +115,9 @@ const BodyTab: React.FunctionComponent<{ const eventPromise = new Promise(f => reader.onload = f); reader.readAsDataURL(blob); setResponseBody({ dataUrl: (await eventPromise).target.result }); + } else if (isFont) { + const font = await response.arrayBuffer(); + setResponseBody({ font }); } else { const formattedBody = formatBody(await response.text(), resource.response.content.mimeType); setResponseBody({ text: formattedBody, mimeType: resource.response.content.mimeType }); @@ -128,11 +132,48 @@ const BodyTab: React.FunctionComponent<{ return
{!resource.response.content._sha1 &&
Response body is not available for this request.
} + {responseBody && responseBody.font && } {responseBody && responseBody.dataUrl && } {responseBody && responseBody.text && }
; }; +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
Could not load font preview
; + + return
+ ABCDEFGHIJKLM
+ NOPQRSTUVWXYZ
+ abcdefghijklm
+ nopqrstuvwxyz
+ 1234567890 +
; +}; + function statusClass(statusCode: number): string { if (statusCode < 300 || statusCode === 304) return 'green-circle'; diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 798025cbbd..4faa668677 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -184,6 +184,7 @@ export const SnapshotTab: React.FunctionComponent<{ setIsInspecting(!isInspecting)} /> {['action', 'before', 'after'].map(tab => { return void }> = ({ tag, style, onClick }) => { +export const TagView = ({ tag, style, onClick }: { tag: string, style?: React.CSSProperties, onClick?: (e: React.MouseEvent) => void }) => { return
{[...statusFilters.entries()].map(([status, value]) => { - return
+ return
{[...projectFilters.entries()].map(([projectName, value]) => { - return
+ return